diff --git a/package.json b/package.json index 02134a2e0c..58d8337540 100644 --- a/package.json +++ b/package.json @@ -100,8 +100,8 @@ "fsm-event": "^2.1.0", "get-folder-size": "^2.0.0", "glob": "^7.1.3", - "hapi": "^16.6.2", - "hapi-set-header": "^1.0.2", + "hapi": "^18.0.0", + "hapi-pino": "^5.2.0", "hoek": "^6.1.2", "human-to-milliseconds": "^1.0.0", "interface-datastore": "~0.6.0", @@ -110,7 +110,7 @@ "ipfs-block-service": "~0.15.1", "ipfs-http-client": "^29.0.0", "ipfs-http-response": "~0.2.1", - "ipfs-mfs": "~0.8.0", + "ipfs-mfs": "0.9.0", "ipfs-multipart": "~0.1.0", "ipfs-repo": "~0.26.1", "ipfs-unixfs": "~0.1.16", diff --git a/src/cli/commands/daemon.js b/src/cli/commands/daemon.js index 71024150de..d14dc07bb8 100644 --- a/src/cli/commands/daemon.js +++ b/src/cli/commands/daemon.js @@ -1,10 +1,6 @@ 'use strict' -const promisify = require('promisify-es6') -const utils = require('../utils') -const print = utils.print - -let httpAPI +const { getRepoPath, print, ipfsPathHelp } = require('../utils') module.exports = { command: 'daemon', @@ -13,7 +9,7 @@ module.exports = { builder (yargs) { return yargs - .epilog(utils.ipfsPathHelp) + .epilog(ipfsPathHelp) .option('enable-sharding-experiment', { type: 'boolean', default: false @@ -40,14 +36,25 @@ module.exports = { argv.resolve((async () => { print('Initializing IPFS daemon...') - const repoPath = utils.getRepoPath() + const repoPath = getRepoPath() // Required inline to reduce startup time - const HttpAPI = require('../../http') - httpAPI = new HttpAPI(process.env.IPFS_PATH, null, argv) + const HttpApi = require('../../http') + const api = new HttpApi({ + silent: argv.silent, + repo: process.env.IPFS_PATH, + offline: argv.offline, + pass: argv.pass, + EXPERIMENTAL: { + pubsub: argv.enablePubsubExperiment, + ipnsPubsub: argv.enableNamesysPubsub, + dht: argv.enableDhtExperiment, + sharding: argv.enableShardingExperiment + } + }) try { - await promisify(httpAPI.start)() + await api.start() } catch (err) { if (err.code === 'ENOENT' && err.message.match(/uninitialized/i)) { print('Error: no initialized ipfs repo found in ' + repoPath) @@ -61,7 +68,7 @@ module.exports = { const cleanup = async () => { print(`Received interrupt signal, shutting down..`) - await promisify(httpAPI.stop)() + await api.stop() process.exit(0) } diff --git a/src/http/api/resources/bitswap.js b/src/http/api/resources/bitswap.js index 9028fce698..8dcc13c51b 100644 --- a/src/http/api/resources/bitswap.js +++ b/src/http/api/resources/bitswap.js @@ -1,12 +1,9 @@ 'use strict' -const boom = require('boom') const Joi = require('joi') const multibase = require('multibase') const { cidToString } = require('../../../utils/cid') -const parseKey = require('./block').parseKey - -exports = module.exports +const { parseKey } = require('./block') exports.wantlist = { validate: { @@ -15,19 +12,17 @@ exports.wantlist = { }).unknown() }, - handler: (request, reply) => { + async handler (request, h) { + const { ipfs } = request.server.app const peerId = request.query.peer const cidBase = request.query['cid-base'] - request.server.app.ipfs.bitswap.wantlist(peerId, (err, list) => { - if (err) { - return reply(boom.badRequest(err)) - } - reply({ - Keys: list.Keys.map(k => ({ - '/': cidToString(k['/'], { base: cidBase, upgrade: false }) - })) - }) + const list = await ipfs.bitswap.wantlist(peerId) + + return h.response({ + Keys: list.Keys.map(k => ({ + '/': cidToString(k['/'], { base: cidBase, upgrade: false }) + })) }) } } @@ -39,33 +34,26 @@ exports.stat = { }).unknown() }, - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const cidBase = request.query['cid-base'] - ipfs.bitswap.stat((err, stats) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } + const stats = await ipfs.bitswap.stat() - stats.wantlist = stats.wantlist.map(k => ({ - '/': cidToString(k['/'], { base: cidBase, upgrade: false }) - })) + stats.wantlist = stats.wantlist.map(k => ({ + '/': cidToString(k['/'], { base: cidBase, upgrade: false }) + })) - reply({ - ProvideBufLen: stats.provideBufLen, - BlocksReceived: stats.blocksReceived, - Wantlist: stats.wantlist, - Peers: stats.peers, - DupBlksReceived: stats.dupBlksReceived, - DupDataReceived: stats.dupDataReceived, - DataReceived: stats.dataReceived, - BlocksSent: stats.blocksSent, - DataSent: stats.dataSent - }) + return h.response({ + ProvideBufLen: stats.provideBufLen, + BlocksReceived: stats.blocksReceived, + Wantlist: stats.wantlist, + Peers: stats.peers, + DupBlksReceived: stats.dupBlksReceived, + DupDataReceived: stats.dupDataReceived, + DataReceived: stats.dataReceived, + BlocksSent: stats.blocksSent, + DataSent: stats.dataSent }) } } @@ -81,14 +69,10 @@ exports.unwant = { parseArgs: parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { + async handler (request, h) { const key = request.pre.args.key - const ipfs = request.server.app.ipfs - ipfs.bitswap.unwant(key, (err) => { - if (err) { - return reply(boom.badRequest(err)) - } - reply({ key: cidToString(key, { base: request.query['cid-base'], upgrade: false }) }) - }) + const { ipfs } = request.server.app + await ipfs.bitswap.unwant(key) + return h.response({ key: cidToString(key, { base: request.query['cid-base'], upgrade: false }) }) } } diff --git a/src/http/api/resources/block.js b/src/http/api/resources/block.js index 929e1a40ab..6dd1d5a524 100644 --- a/src/http/api/resources/block.js +++ b/src/http/api/resources/block.js @@ -4,29 +4,23 @@ const CID = require('cids') const multipart = require('ipfs-multipart') const Joi = require('joi') const multibase = require('multibase') +const Boom = require('boom') const { cidToString } = require('../../../utils/cid') const debug = require('debug') const log = debug('jsipfs:http-api:block') log.error = debug('jsipfs:http-api:block:error') -exports = module.exports - // common pre request handler that parses the args and returns `key` which is assigned to `request.pre.args` -exports.parseKey = (request, reply) => { +exports.parseKey = (request, h) => { if (!request.query.arg) { - return reply("Argument 'key' is required").code(400).takeover() + throw Boom.badRequest("Argument 'key' is required") } try { - return reply({ - key: new CID(request.query.arg) - }) + return { key: new CID(request.query.arg) } } catch (err) { log.error(err) - return reply({ - Message: 'Not a valid hash', - Code: 0 - }).code(500).takeover() + throw Boom.badRequest('Not a valid hash') } } @@ -35,27 +29,21 @@ exports.get = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { + async handler (request, h) { const key = request.pre.args.key - request.server.app.ipfs.block.get(key, (err, block) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get block: ' + err, - Code: 0 - }).code(500) - } - - if (block) { - return reply(block.data).header('X-Stream-Output', '1') - } - - return reply({ - Message: 'Block was unwanted before it could be remotely retrieved', - Code: 0 - }).code(404) - }) + let block + try { + block = await request.server.app.ipfs.block.get(key) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get block' }) + } + + if (!block) { + throw Boom.notFound('Block was unwanted before it could be remotely retrieved') + } + + return h.response(block.data).header('X-Stream-Output', '1') } } @@ -67,61 +55,52 @@ exports.put = { }, // pre request handler that parses the args and returns `data` which is assigned to `request.pre.args` - parseArgs: (request, reply) => { + parseArgs: (request, h) => { if (!request.payload) { - return reply({ - Message: "File argument 'data' is required", - Code: 0 - }).code(400).takeover() + throw Boom.badRequest("File argument 'data' is required") } - const parser = multipart.reqParser(request.payload) - var file + return new Promise((resolve, reject) => { + const parser = multipart.reqParser(request.payload) + let file - parser.on('file', (fileName, fileStream) => { - file = Buffer.alloc(0) + parser.on('file', (fileName, fileStream) => { + file = Buffer.alloc(0) - fileStream.on('data', (data) => { - file = Buffer.concat([file, data]) + fileStream.on('data', (data) => { + file = Buffer.concat([file, data]) + }) }) - }) - parser.on('end', () => { - if (!file) { - return reply({ - Message: "File argument 'data' is required", - Code: 0 - }).code(400).takeover() - } + parser.on('end', () => { + if (!file) { + return reject(Boom.badRequest("File argument 'data' is required")) + } - return reply({ - data: file + resolve({ data: file }) }) }) }, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const data = request.pre.args.data - const ipfs = request.server.app.ipfs - - ipfs.block.put(data, { - mhtype: request.query.mhtype, - format: request.query.format, - version: request.query.version && parseInt(request.query.version) - }, (err, block) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to put block: ' + err, - Code: 0 - }).code(500) - } - - return reply({ - Key: cidToString(block.cid, { base: request.query['cid-base'] }), - Size: block.data.length + async handler (request, h) { + const { data } = request.pre.args + const { ipfs } = request.server.app + + let block + try { + block = await ipfs.block.put(data, { + mhtype: request.query.mhtype, + format: request.query.format, + version: request.query.version && parseInt(request.query.version) }) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to put block' }) + } + + return h.response({ + Key: cidToString(block.cid, { base: request.query['cid-base'] }), + Size: block.data.length }) } } @@ -131,20 +110,16 @@ exports.rm = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key + async handler (request, h) { + const { key } = request.pre.args - request.server.app.ipfs.block.rm(key, (err, block) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to delete block: ' + err, - Code: 0 - }).code(500) - } + try { + await request.server.app.ipfs.block.rm(key) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to delete block' }) + } - return reply() - }) + return h.response() } } @@ -159,22 +134,19 @@ exports.stat = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key + async handler (request, h) { + const { key } = request.pre.args + + let stats + try { + stats = await request.server.app.ipfs.block.stat(key) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get block stats' }) + } - request.server.app.ipfs.block.stat(key, (err, stats) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get block stats: ' + err, - Code: 0 - }).code(500) - } - - return reply({ - Key: cidToString(stats.key, { base: request.query['cid-base'] }), - Size: stats.size - }) + return h.response({ + Key: cidToString(stats.key, { base: request.query['cid-base'] }), + Size: stats.size }) } } diff --git a/src/http/api/resources/bootstrap.js b/src/http/api/resources/bootstrap.js index affc3a2126..f7f3a44736 100644 --- a/src/http/api/resources/bootstrap.js +++ b/src/http/api/resources/bootstrap.js @@ -1,90 +1,76 @@ 'use strict' const multiaddr = require('multiaddr') +const Boom = require('boom') -exports = module.exports - -function applyError (reply, err) { - reply({ - Message: err.message, - Code: 0 - }).code(500).takeover() -} - -exports.list = (request, reply) => { - const ipfs = request.server.app.ipfs - - ipfs.bootstrap.list((err, list) => { - if (err) { - return applyError(reply, err) - } - - return reply(list) - }) +exports.list = async (request, h) => { + const { ipfs } = request.server.app + const list = await ipfs.bootstrap.list() + return h.response(list) } exports.add = { - parseArgs (request, reply) { + parseArgs (request, h) { const q = request.query const def = q.default === 'true' if (q.arg != null) { try { - return reply({ + return { addr: multiaddr(q.arg), default: def - }) + } } catch (err) { - return applyError(reply, new Error('Not a valid multiaddr')) + throw Boom.badRequest('Not a valid multiaddr') } - } else { - reply({ default: def }) } - }, - handler (request, reply) { - const ipfs = request.server.app.ipfs - const addr = request.pre.args.addr - const def = request.pre.args.default - ipfs.bootstrap.add(addr && addr.toString(), { default: def }, (err, list) => { - if (err) { - return applyError(reply, err) - } + console.log('parseArgs', { default: def }) - return reply(list) - }) + return { default: def } + }, + async handler (request, h) { + const { ipfs } = request.server.app + const { addr, default: def } = request.pre.args + const list = await ipfs.bootstrap.add(addr && addr.toString(), { default: def }) + return h.response(list) } } +exports.addDefault = async (request, h) => { + const { ipfs } = request.server.app + const list = await ipfs.bootstrap.add(null, { default: true }) + return h.response(list) +} + exports.rm = { - parseArgs (request, reply) { + parseArgs (request, h) { const q = request.query const all = q.all === 'true' if (q.arg != null) { try { - return reply({ + return { addr: multiaddr(q.arg), all: all - }) + } } catch (err) { - return applyError(reply, new Error('Not a valid multiaddr')) + throw Boom.badRequest('Not a valid multiaddr') } - } else { - reply({ all: all }) } - }, - handler (request, reply) { - const ipfs = request.server.app.ipfs - const addr = request.pre.args.addr - const all = request.pre.args.all - - ipfs.bootstrap.rm(addr && addr.toString(), { all: all }, (err, list) => { - if (err) { - return applyError(reply, err) - } - return reply(list) - }) + return { all } + }, + async handler (request, h) { + const { ipfs } = request.server.app + const { addr, all } = request.pre.args + const list = await ipfs.bootstrap.rm(addr && addr.toString(), { all }) + return h.response(list) } } + +exports.rmAll = async (request, h) => { + const { ipfs } = request.server.app + const list = await ipfs.bootstrap.rm(null, { all: true }) + return h.response(list) +} diff --git a/src/http/api/resources/config.js b/src/http/api/resources/config.js index 9ad56080ea..364d19315d 100644 --- a/src/http/api/resources/config.js +++ b/src/http/api/resources/config.js @@ -6,12 +6,11 @@ const set = require('lodash/set') const log = debug('jsipfs:http-api:config') log.error = debug('jsipfs:http-api:config:error') const multipart = require('ipfs-multipart') - -exports = module.exports +const Boom = require('boom') exports.getOrSet = { // pre request handler that parses the args and returns `key` & `value` which are assigned to `request.pre.args` - parseArgs: (request, reply) => { + parseArgs (request, h) { const parseValue = (args) => { if (request.query.bool !== undefined) { args.value = args.value === 'true' @@ -20,14 +19,11 @@ exports.getOrSet = { args.value = JSON.parse(args.value) } catch (err) { log.error(err) - return reply({ - Message: 'failed to unmarshal json. ' + err, - Code: 0 - }).code(500).takeover() + throw Boom.badRequest('failed to unmarshal json. ' + err) } } - return reply(args) + return args } if (request.query.arg instanceof Array) { @@ -45,172 +41,119 @@ exports.getOrSet = { } if (!request.query.arg) { - return reply("Argument 'key' is required").code(400).takeover() + throw Boom.badRequest("Argument 'key' is required") } - return reply({ - key: request.query.arg - }) + return { key: request.query.arg } }, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key - const value = request.pre.args.value - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app + const { key } = request.pre.args + let { value } = request.pre.args // check that value exists - typeof null === 'object' if (value && (typeof value === 'object' && value.type === 'Buffer')) { - return reply({ - Message: 'Invalid value type', - Code: 0 - }).code(500) + throw Boom.badRequest('Invalid value type') } - if (value === undefined) { - // Get the value of a given key - return ipfs.config.get((err, config) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get config value: ' + err, - Code: 0 - }).code(500) - } - - const value = get(config, key) - if (value === undefined) { - return reply({ - Message: 'Failed to get config value: key has no attributes', - Code: 0 - }).code(500) - } - - return reply({ - Key: key, - Value: value - }) - }) + let originalConfig + try { + originalConfig = await ipfs.config.get() + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get config value' }) } - // Set the new value of a given key - ipfs.config.get((err, originalConfig) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get config value: ' + err, - Code: 0 - }).code(500) + if (value === undefined) { + // Get the value of a given key + value = get(originalConfig, key) + if (value === undefined) { + throw Boom.notFound('Failed to get config value: key has no attributes') } - + } else { + // Set the new value of a given key const updatedConfig = set(originalConfig, key, value) - ipfs.config.replace(updatedConfig, (err) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get config value: ' + err, - Code: 0 - }).code(500) - } + try { + await ipfs.config.replace(updatedConfig) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get config value' }) + } + } - return reply({ - Key: key, - Value: value - }) - }) + return h.response({ + Key: key, + Value: value }) } } -exports.get = (request, reply) => { - const ipfs = request.server.app.ipfs +exports.get = async (request, h) => { + const { ipfs } = request.server.app - ipfs.config.get((err, config) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get config value: ' + err, - Code: 0 - }).code(500) - } + let config + try { + config = await ipfs.config.get() + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get config value' }) + } - return reply({ - Value: config - }) + return h.response({ + Value: config }) } -exports.show = (request, reply) => { - const ipfs = request.server.app.ipfs +exports.show = async (request, h) => { + const { ipfs } = request.server.app - ipfs.config.get((err, config) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get config value: ' + err, - Code: 0 - }).code(500) - } + let config + try { + config = await ipfs.config.get() + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get config value' }) + } - return reply(config) - }) + return h.response(config) } exports.replace = { // pre request handler that parses the args and returns `config` which is assigned to `request.pre.args` - parseArgs: (request, reply) => { + async parseArgs (request, h) { if (!request.payload) { - return reply({ - Message: "Argument 'file' is required", - Code: 1123 - - }).code(400).takeover() + throw Boom.badRequest("Argument 'file' is required") } - const parser = multipart.reqParser(request.payload) - var file - - parser.on('file', (fileName, fileStream) => { - fileStream.on('data', (data) => { - file = data - }) + const fileStream = await new Promise((resolve, reject) => { + multipart.reqParser(request.payload) + .on('file', (fileName, fileStream) => resolve(fileStream)) + .on('end', () => reject(Boom.badRequest("Argument 'file' is required"))) }) - parser.on('end', () => { - if (!file) { - return reply({ - Message: "Argument 'file' is required", - Code: 1123 - - }).code(400).takeover() - } - - try { - return reply({ - config: JSON.parse(file.toString()) - }) - } catch (err) { - return reply({ - Message: 'Failed to decode file as config: ' + err, - Code: 0 - }).code(500).takeover() - } + const file = await new Promise((resolve, reject) => { + fileStream + .on('data', data => resolve(data)) + .on('end', () => reject(Boom.badRequest("Argument 'file' is required"))) }) + + try { + return { config: JSON.parse(file.toString()) } + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to decode file as config' }) + } }, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - return request.server.app.ipfs.config.replace(request.pre.args.config, (err) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to save config: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { config } = request.pre.args + + try { + await ipfs.config.replace(config) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to save config' }) + } - return reply() - }) + return h.response() } } diff --git a/src/http/api/resources/dns.js b/src/http/api/resources/dns.js index 92549e68b7..db20a9f170 100644 --- a/src/http/api/resources/dns.js +++ b/src/http/api/resources/dns.js @@ -1,24 +1,14 @@ 'use strict' -const boom = require('boom') +const Boom = require('boom') -exports = module.exports - -exports.get = (request, reply) => { +module.exports = async (request, h) => { if (!request.query.arg) { - return reply({ - Message: "Argument 'domain' is required", - Code: 0 - }).code(400).takeover() + throw Boom.badRequest("Argument 'domain' is required") } - request.server.app.ipfs.dns(request.query.arg, (err, path) => { - if (err) { - return reply(boom.badRequest(err)) - } - - return reply({ - Path: path - }) + const path = await request.server.app.ipfs.dns(request.query.arg) + return h.response({ + Path: path }) } diff --git a/src/http/api/resources/file.js b/src/http/api/resources/file.js index b786bebc19..a56c079256 100644 --- a/src/http/api/resources/file.js +++ b/src/http/api/resources/file.js @@ -1,15 +1,11 @@ 'use strict' -const mh = require('multihashes') -const debug = require('debug') -const log = debug('jsipfs:http-api:file') -log.error = debug('jsipfs:http-api:file:error') +const isIpfs = require('is-ipfs') const unixfsEngine = require('ipfs-unixfs-engine') const exporter = unixfsEngine.exporter const pull = require('pull-stream') const toB58String = require('multihashes').toB58String - -exports = module.exports +const Boom = require('boom') const fileTypeMap = { file: 'File', @@ -29,12 +25,9 @@ function toFileObject (file) { } // common pre request handler that parses the args and returns `key` which is assigned to `request.pre.args` -exports.parseKey = (request, reply) => { +exports.parseKey = (request, h) => { if (!request.query.arg) { - return reply({ - Message: "Argument 'key' is required", - Code: 0 - }).code(400).takeover() + throw Boom.badRequest("Argument 'key' is required") } let key = request.query.arg @@ -48,24 +41,18 @@ exports.parseKey = (request, reply) => { hash = hash.substring(0, slashIndex) } - try { - mh.fromB58String(hash) - } catch (err) { - log.error(err) - return reply({ - Message: 'invalid ipfs ref path', - Code: 0 - }).code(500).takeover() + if (!isIpfs.ipfsPath(request.query.arg) && !isIpfs.cid(request.query.arg)) { + throw Boom.badRequest('invalid ipfs ref path') } const subpaths = key.split('/') subpaths.shift() - reply({ + return { path: request.query.arg, subpaths: subpaths, key: key, hash: hash - }) + } } exports.ls = { @@ -73,39 +60,37 @@ exports.ls = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const path = request.pre.args.path - const ipfs = request.server.app.ipfs - const subpaths = request.pre.args.subpaths + async handler (request, h) { + const { ipfs } = request.server.app + const { path, subpaths } = request.pre.args.path const rootDepth = subpaths.length - pull( - exporter(path, ipfs._ipld, { maxDepth: rootDepth + 1 }), - pull.collect((err, files) => { - if (err) { - return reply({ - Message: 'Failed to list dir: ' + err.message, - Code: 0 - }).code(500) - } - - let res = { - Arguments: {}, - Objects: {} - } - const links = [] - files.forEach((file) => { - if (file.depth === rootDepth) { - let id = toB58String(file.hash) - res.Arguments[path] = id - res.Objects[id] = toFileObject(file) - res.Objects[id].Links = file.type === 'file' ? null : links - } else { - links.push(toFileObject(file)) - } + const files = await new Promise((resolve, reject) => { + pull( + exporter(path, ipfs._ipld, { maxDepth: rootDepth + 1 }), + pull.collect((err, files) => { + if (err) return reject(err) + resolve(files) }) - return reply(res) - }) - ) + ) + }) + + const res = { + Arguments: {}, + Objects: {} + } + const links = [] + files.forEach((file) => { + if (file.depth === rootDepth) { + const id = toB58String(file.hash) + res.Arguments[path] = id + res.Objects[id] = toFileObject(file) + res.Objects[id].Links = file.type === 'file' ? null : links + } else { + links.push(toFileObject(file)) + } + }) + + return h.response(res) } } diff --git a/src/http/api/resources/files-regular.js b/src/http/api/resources/files-regular.js index d0944e250e..0ee0c35ca7 100644 --- a/src/http/api/resources/files-regular.js +++ b/src/http/api/resources/files-regular.js @@ -1,6 +1,5 @@ 'use strict' -const CID = require('cids') const multipart = require('ipfs-multipart') const debug = require('debug') const tar = require('tar-stream') @@ -9,17 +8,17 @@ log.error = debug('jsipfs:http-api:files:error') const pull = require('pull-stream') const toPull = require('stream-to-pull-stream') const pushable = require('pull-pushable') -const each = require('async/each') const toStream = require('pull-stream-to-stream') const abortable = require('pull-abortable') const Joi = require('joi') +const Boom = require('boom') const ndjson = require('pull-ndjson') const { PassThrough } = require('readable-stream') const multibase = require('multibase') +const isIpfs = require('is-ipfs') +const promisify = require('promisify-es6') const { cidToString } = require('../../../utils/cid') -exports = module.exports - function numberFromQuery (query, key) { if (query && query[key] !== undefined) { const value = parseInt(query[key], 10) @@ -33,43 +32,24 @@ function numberFromQuery (query, key) { } // common pre request handler that parses the args and returns `key` which is assigned to `request.pre.args` -exports.parseKey = (request, reply) => { - if (!request.query.arg) { - return reply({ - Message: "Argument 'key' is required", - Code: 0, - Type: 'error' - }).code(400).takeover() - } - - let key = request.query.arg - if (key.indexOf('/ipfs/') === 0) { - key = key.substring(6) - } +exports.parseKey = (request, h) => { + const { arg } = request.query - const slashIndex = key.indexOf('/') - if (slashIndex > 0) { - key = key.substring(0, slashIndex) + if (!arg) { + throw Boom.badRequest("Argument 'key' is required") } - try { - new CID(key) // eslint-disable-line no-new - } catch (err) { - log.error(err) - return reply({ - Message: 'invalid ipfs ref path', - Code: 0, - Type: 'error' - }).code(500).takeover() + if (!isIpfs.ipfsPath(arg) && !isIpfs.cid(arg) && !isIpfs.ipfsPath('/ipfs/' + arg)) { + throw Boom.badRequest('invalid ipfs ref path') } - reply({ - key: request.query.arg, + return { + key: arg, options: { offset: numberFromQuery(request.query, 'offset'), length: numberFromQuery(request.query, 'length') } - }) + } } exports.cat = { @@ -77,46 +57,48 @@ exports.cat = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key - const options = request.pre.args.options - const ipfs = request.server.app.ipfs - - let pusher - let started = false - - pull( - ipfs.catPullStream(key, options), - pull.drain( - chunk => { - if (!started) { - started = true - pusher = pushable() - reply(toStream.source(pusher).pipe(new PassThrough())) - .header('X-Stream-Output', '1') - } - pusher.push(chunk) - }, - err => { - if (err) { - log.error(err) - - // We already started flowing, abort the stream - if (started) { - return pusher.end(err) + async handler (request, h) { + const { ipfs } = request.server.app + const { key, options } = request.pre.args + + const stream = await new Promise((resolve, reject) => { + let pusher + let started = false + + pull( + ipfs.catPullStream(key, options), + pull.drain( + chunk => { + if (!started) { + started = true + pusher = pushable() + resolve(toStream.source(pusher).pipe(new PassThrough())) + } + pusher.push(chunk) + }, + err => { + if (err) { + log.error(err) + + // We already started flowing, abort the stream + if (started) { + return pusher.end(err) + } + + err.message = err.message === 'No such file' + ? err.message + : 'Failed to cat file: ' + err + + return reject(err) } - const msg = err.message === 'No such file' - ? err.message - : 'Failed to cat file: ' + err - - return reply({ Message: msg, Code: 0, Type: 'error' }).code(500) + pusher.end() } - - pusher.end() - } + ) ) - ) + }) + + return h.response(stream).header('X-Stream-Output', '1') } } @@ -125,44 +107,42 @@ exports.get = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app + const { key } = request.pre.args const pack = tar.pack() - ipfs.get(key, (err, filesArray) => { - if (err) { - log.error(err) - pack.emit('error', err) - pack.destroy() - return - } + let filesArray + try { + filesArray = await ipfs.get(key) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get key' }) + } + + pack.entry = promisify(pack.entry.bind(pack)) - each(filesArray, (file, cb) => { + Promise + .all(filesArray.map(file => { const header = { name: file.path } if (file.content) { header.size = file.size - pack.entry(header, file.content, cb) + return pack.entry(header, file.content) } else { header.type = 'directory' - pack.entry(header, cb) + return pack.entry(header) } - }, (err) => { - if (err) { - log.error(err) - pack.emit('error', err) - pack.destroy() - return - } - - pack.finalize() + })) + .then(() => pack.finalize()) + .catch(err => { + log.error(err) + pack.emit('error', err) + pack.destroy() }) - // reply must be called right away so that tar-stream offloads its content - // otherwise it will block in large files - reply(pack).header('X-Stream-Output', '1') - }) + // reply must be called right away so that tar-stream offloads its content + // otherwise it will block in large files + return h.response(pack).header('X-Stream-Output', '1') } } @@ -182,56 +162,48 @@ exports.add = { .options({ allowUnknown: true }) }, - handler: (request, reply) => { + async handler (request, h) { if (!request.payload) { - return reply({ - Message: 'Array, Buffer, or String is required.', - Code: 0, - Type: 'error' - }).code(400).takeover() + throw Boom.badRequest('Array, Buffer, or String is required.') } - const ipfs = request.server.app.ipfs - // TODO: make pull-multipart - const parser = multipart.reqParser(request.payload) - let filesParsed = false - - const fileAdder = pushable() - - parser.on('file', (fileName, fileStream) => { - fileName = decodeURIComponent(fileName) - const filePair = { - path: fileName, - content: toPull(fileStream) - } - filesParsed = true - fileAdder.push(filePair) - }) + const { ipfs } = request.server.app + + const fileAdder = await new Promise((resolve, reject) => { + // TODO: make pull-multipart + const parser = multipart.reqParser(request.payload) + let filesParsed = false + const adder = pushable() + + parser.on('file', (fileName, fileStream) => { + if (!filesParsed) { + resolve(adder) + filesParsed = true + } - parser.on('directory', (directory) => { - directory = decodeURIComponent(directory) + adder.push({ + path: decodeURIComponent(fileName), + content: toPull(fileStream) + }) + }) - fileAdder.push({ - path: directory, - content: '' + parser.on('directory', (dirName) => { + adder.push({ + path: decodeURIComponent(dirName), + content: '' + }) }) - }) - parser.on('end', () => { - if (!filesParsed) { - return reply({ - Message: "File argument 'data' is required.", - Code: 0, - Type: 'error' - }).code(400).takeover() - } - fileAdder.end() + parser.on('end', () => { + if (!filesParsed) { + reject(new Error("File argument 'data' is required.")) + } + adder.end() + }) }) const replyStream = pushable() - const progressHandler = (bytes) => { - replyStream.push({ Bytes: bytes }) - } + const progressHandler = bytes => replyStream.push({ Bytes: bytes }) const options = { cidVersion: request.query['cid-version'], @@ -261,42 +233,42 @@ exports.add = { stream._readableState = {} stream.unpipe = () => {} } - reply(stream) - .header('x-chunked-output', '1') - .header('content-type', 'application/json') - .header('Trailer', 'X-Stream-Error') - function _writeErr (msg, code) { - const err = JSON.stringify({ Message: msg, Code: code }) - request.raw.res.addTrailers({ - 'X-Stream-Error': err - }) - return aborter.abort() - } + let filesAdded = false pull( fileAdder, ipfs.addPullStream(options), - pull.map((file) => { - return { - Name: file.path, // addPullStream already turned this into a hash if it wanted to - Hash: cidToString(file.hash, { base: request.query['cid-base'] }), - Size: file.size - } - }), - pull.collect((err, files) => { - if (err) { - return _writeErr(err, 0) - } + pull.map(file => ({ + Name: file.path, // addPullStream already turned this into a hash if it wanted to + Hash: cidToString(file.hash, { base: request.query['cid-base'] }), + Size: file.size + })), + pull.drain( + file => { + replyStream.push(file) + filesAdded = true + }, + err => { + if (err || !filesAdded) { + request.raw.res.addTrailers({ + 'X-Stream-Error': JSON.stringify({ + Message: err ? err.message : 'Failed to add files.', + Code: 0 + }) + }) + return aborter.abort() + } - if (files.length === 0 && filesParsed) { - return _writeErr('Failed to add files.', 0) + replyStream.end() } - - files.forEach((f) => replyStream.push(f)) - replyStream.end() - }) + ) ) + + return h.response(stream) + .header('x-chunked-output', '1') + .header('content-type', 'application/json') + .header('Trailer', 'X-Stream-Error') } } @@ -311,33 +283,30 @@ exports.ls = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { + async handler (request, h) { + const { ipfs } = request.server.app const { key } = request.pre.args - const ipfs = request.server.app.ipfs const recursive = request.query && request.query.recursive === 'true' const cidBase = request.query['cid-base'] - ipfs.ls(key, { recursive }, (err, files) => { - if (err) { - return reply({ - Message: 'Failed to list dir: ' + err.message, - Code: 0, - Type: 'error' - }).code(500).takeover() - } - - reply({ - Objects: [{ - Hash: key, - Links: files.map((file) => ({ - Name: file.name, - Hash: cidToString(file.hash, { base: cidBase }), - Size: file.size, - Type: toTypeCode(file.type), - Depth: file.depth - })) - }] - }) + let files + try { + files = await ipfs.ls(key, { recursive }) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to list dir' }) + } + + return h.response({ + Objects: [{ + Hash: key, + Links: files.map((file) => ({ + Name: file.name, + Hash: cidToString(file.hash, { base: cidBase }), + Size: file.size, + Type: toTypeCode(file.type), + Depth: file.depth + })) + }] }) } } diff --git a/src/http/api/resources/id.js b/src/http/api/resources/id.js index d05433b329..2ba8218966 100644 --- a/src/http/api/resources/id.js +++ b/src/http/api/resources/id.js @@ -1,21 +1,12 @@ 'use strict' -const boom = require('boom') - -exports = module.exports - -exports.get = (request, reply) => { - request.server.app.ipfs.id((err, id) => { - if (err) { - return reply(boom.badRequest(err)) - } - - return reply({ - ID: id.id, - PublicKey: id.publicKey, - Addresses: id.addresses, - AgentVersion: id.agentVersion, - ProtocolVersion: id.protocolVersion - }) +exports.get = async (request, h) => { + const id = await request.server.app.ipfs.id() + return h.response({ + ID: id.id, + PublicKey: id.publicKey, + Addresses: id.addresses, + AgentVersion: id.agentVersion, + ProtocolVersion: id.protocolVersion }) } diff --git a/src/http/api/resources/key.js b/src/http/api/resources/key.js index 0e5e1545fc..cd035a4bad 100644 --- a/src/http/api/resources/key.js +++ b/src/http/api/resources/key.js @@ -1,14 +1,5 @@ 'use strict' -exports = module.exports - -function applyError (reply, err) { - reply({ - Message: err.message, - Code: 0 - }).code(500).takeover() -} - function toKeyInfo (key) { return { Name: key.name, @@ -16,87 +7,48 @@ function toKeyInfo (key) { } } -exports.list = (request, reply) => { - const ipfs = request.server.app.ipfs - - ipfs._keychain.listKeys((err, keys) => { - if (err) { - return applyError(reply, err) - } - - keys = keys.map(toKeyInfo) - return reply({ Keys: keys }) - }) +exports.list = async (request, h) => { + const { ipfs } = request.server.app + const keys = await ipfs.key.list() + return h.response({ Keys: keys.map(toKeyInfo) }) } -exports.rm = (request, reply) => { - const ipfs = request.server.app.ipfs +exports.rm = async (request, h) => { + const { ipfs } = request.server.app const name = request.query.arg - ipfs._keychain.removeKey(name, (err, key) => { - if (err) { - return applyError(reply, err) - } - - return reply({ Keys: [ toKeyInfo(key) ] }) - }) -} - -exports.rename = (request, reply) => { - const ipfs = request.server.app.ipfs - const oldName = request.query.arg[0] - const newName = request.query.arg[1] - ipfs._keychain.renameKey(oldName, newName, (err, key) => { - if (err) { - return applyError(reply, err) - } - - const result = { - Was: oldName, - Now: key.name, - Id: key.id, - Overwrite: false - } - return reply(result) + const key = await ipfs.key.rm(name) + return h.response({ Keys: [ toKeyInfo(key) ] }) +} + +exports.rename = async (request, h) => { + const { ipfs } = request.server.app + const [ oldName, newName ] = request.query.arg + const key = await ipfs.key.rename(oldName, newName) + return h.response({ + Was: key.was, + Now: key.name, + Id: key.id, + Overwrite: key.overwrite }) } -exports.gen = (request, reply) => { - const ipfs = request.server.app.ipfs - const name = request.query.arg - const type = request.query.type - const size = parseInt(request.query.size) - ipfs._keychain.createKey(name, type, size, (err, key) => { - if (err) { - return applyError(reply, err) - } - - return reply(toKeyInfo(key)) - }) +exports.gen = async (request, h) => { + const { ipfs } = request.server.app + const { arg, type, size } = request.query + const key = await ipfs.key.gen(arg, { type, size: parseInt(size) }) + return h.response(toKeyInfo(key)) } -exports.export = (request, reply) => { - const ipfs = request.server.app.ipfs - const name = request.query.arg - const password = request.query.password - ipfs._keychain.exportKey(name, password, (err, pem) => { - if (err) { - return applyError(reply, err) - } - - return reply(pem).type('application/x-pem-file') - }) +exports.export = async (request, h) => { + const { ipfs } = request.server.app + const { arg: name, password } = request.query + const pem = await ipfs.key.export(name, password) + return h.response(pem).type('application/x-pem-file') } -exports.import = (request, reply) => { - const ipfs = request.server.app.ipfs - const name = request.query.arg - const pem = request.query.pem - const password = request.query.password - ipfs._keychain.importKey(name, pem, password, (err, key) => { - if (err) { - return applyError(reply, err) - } - - return reply(toKeyInfo(key)) - }) +exports.import = async (request, h) => { + const { ipfs } = request.server.app + const { arg: name, pem, password } = request.query + const key = await ipfs.key.import(name, pem, password) + return h.response(toKeyInfo(key)) } diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js index be2c9eaad5..9a4182f42e 100644 --- a/src/http/api/resources/name.js +++ b/src/http/api/resources/name.js @@ -2,8 +2,6 @@ const Joi = require('joi') -exports = module.exports - exports.resolve = { validate: { query: Joi.object().keys({ @@ -12,21 +10,14 @@ exports.resolve = { recursive: Joi.boolean().default(false) }).unknown() }, - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const { arg } = request.query - ipfs.name.resolve(arg, request.query, (err, res) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } + const res = await ipfs.name.resolve(arg, request.query) - return reply({ - Path: res.path - }).code(200) + return h.response({ + Path: res.path }) } } @@ -40,60 +31,39 @@ exports.publish = { key: Joi.string().default('self') }).unknown() }, - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const { arg } = request.query - ipfs.name.publish(arg, request.query, (err, res) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } + const res = await ipfs.name.publish(arg, request.query) - return reply({ - Name: res.name, - Value: res.value - }).code(200) + return h.response({ + Name: res.name, + Value: res.value }) } } exports.pubsub = { state: { - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app - ipfs.name.pubsub.state((err, res) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } + const res = await ipfs.name.pubsub.state() - return reply({ - Enabled: res.enabled - }).code(200) + return h.response({ + Enabled: res.enabled }) } }, subs: { - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app - ipfs.name.pubsub.subs((err, res) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } + const res = await ipfs.name.pubsub.subs() - return reply({ - Strings: res - }).code(200) + return h.response({ + Strings: res }) } }, @@ -103,21 +73,14 @@ exports.pubsub = { arg: Joi.string().required() }).unknown() }, - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const { arg } = request.query - ipfs.name.pubsub.cancel(arg, (err, res) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } + const res = await ipfs.name.pubsub.cancel(arg) - return reply({ - Canceled: res.canceled - }).code(200) + return h.response({ + Canceled: res.canceled }) } } diff --git a/src/http/api/resources/object.js b/src/http/api/resources/object.js index e3ed14d72f..0c192adcd9 100644 --- a/src/http/api/resources/object.js +++ b/src/http/api/resources/object.js @@ -1,36 +1,32 @@ 'use strict' +const promisify = require('promisify-es6') const CID = require('cids') const multipart = require('ipfs-multipart') const dagPB = require('ipld-dag-pb') -const DAGLink = dagPB.DAGLink -const DAGNode = dagPB.DAGNode -const waterfall = require('async/waterfall') +const { DAGNode, DAGLink } = dagPB +const calculateCid = promisify(dagPB.util.cid) +const deserialize = promisify(dagPB.util.deserialize) +const createDagNode = promisify(DAGNode.create) const Joi = require('joi') const multibase = require('multibase') +const Boom = require('boom') const { cidToString } = require('../../../utils/cid') const debug = require('debug') const log = debug('jsipfs:http-api:object') log.error = debug('jsipfs:http-api:object:error') -exports = module.exports - // common pre request handler that parses the args and returns `key` which is assigned to `request.pre.args` -exports.parseKey = (request, reply) => { +exports.parseKey = (request, h) => { if (!request.query.arg) { - return reply("Argument 'key' is required").code(400).takeover() + throw Boom.badRequest("Argument 'key' is required") } try { - return reply({ - key: new CID(request.query.arg) - }) + return { key: new CID(request.query.arg) } } catch (err) { log.error(err) - return reply({ - Message: 'invalid ipfs ref path', - Code: 0 - }).code(500).takeover() + throw Boom.badRequest('invalid ipfs ref path') } } @@ -41,39 +37,34 @@ exports.new = { }).unknown() }, - handler (request, reply) { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const template = request.query.arg - waterfall([ - (cb) => ipfs.object.new(template, cb), - (cid, cb) => ipfs.object.get(cid, (err, node) => cb(err, { node, cid })) - ], (err, results) => { - if (err) { - log.error(err) - return reply({ - Message: `Failed to create object: ${err.message}`, - Code: 0 - }).code(500) - } + let cid, node + try { + cid = await ipfs.object.new(template) + node = await ipfs.object.get(cid) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to create object' }) + } - const nodeJSON = results.node.toJSON() - - const answer = { - Data: nodeJSON.data, - Hash: cidToString(results.cid, { base: request.query['cid-base'], upgrade: false }), - Size: nodeJSON.size, - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) - } + const nodeJSON = node.toJSON() + + const answer = { + Data: nodeJSON.data, + Hash: cidToString(cid, { base: request.query['cid-base'], upgrade: false }), + Size: nodeJSON.size, + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } + }) + } - return reply(answer) - }) + return h.response(answer) } } @@ -88,44 +79,39 @@ exports.get = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key + async handler (request, h) { + const { key } = request.pre.args const enc = request.query.enc || 'base58' - const ipfs = request.server.app.ipfs - - waterfall([ - (cb) => ipfs.object.get(key, { enc: enc }, cb), - (node, cb) => dagPB.util.cid(node, (err, cid) => cb(err, { node, cid })) - ], (err, results) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get object: ' + err, - Code: 0 - }).code(500) - } + const { ipfs } = request.server.app - const nodeJSON = results.node.toJSON() + let node, cid + try { + node = await ipfs.object.get(key, { enc: enc }) + cid = await calculateCid(node) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get object' }) + } - if (Buffer.isBuffer(results.node.data)) { - nodeJSON.data = results.node.data.toString(request.query['data-encoding'] || undefined) - } + const nodeJSON = node.toJSON() - const answer = { - Data: nodeJSON.data, - Hash: cidToString(results.cid, { base: request.query['cid-base'], upgrade: false }), - Size: nodeJSON.size, - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) - } + if (Buffer.isBuffer(node.data)) { + nodeJSON.data = node.data.toString(request.query['data-encoding'] || undefined) + } - return reply(answer) - }) + const answer = { + Data: nodeJSON.data, + Hash: cidToString(cid, { base: request.query['cid-base'], upgrade: false }), + Size: nodeJSON.size, + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } + }) + } + + return h.response(answer) } } @@ -138,119 +124,75 @@ exports.put = { // pre request handler that parses the args and returns `node` // which is assigned to `request.pre.args` - parseArgs: (request, reply) => { + async parseArgs (request, h) { if (!request.payload) { - return reply("File argument 'data' is required").code(400).takeover() + throw Boom.badRequest("File argument 'data' is required") } const enc = request.query.inputenc - const parser = multipart.reqParser(request.payload) - - let file - let finished = true - - // TODO: this whole function this to be revisited - // so messy - parser.on('file', (name, stream) => { - finished = false - // TODO fix: stream is not emitting the 'end' event - stream.on('data', (data) => { - if (enc === 'protobuf') { - waterfall([ - (cb) => dagPB.util.deserialize(data, cb), - (node, cb) => dagPB.util.cid(node, (err, cid) => cb(err, { node, cid })) - ], (err, results) => { - if (err) { - return reply({ - Message: 'Failed to put object: ' + err, - Code: 0 - }).code(500).takeover() - } - - const nodeJSON = results.node.toJSON() - - const answer = { - Data: nodeJSON.data, - Hash: results.cid.toBaseEncodedString(), - Size: nodeJSON.size, - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: l.cid - } - }) - } - - file = Buffer.from(JSON.stringify(answer)) - finished = true - }) - } else { - file = data - - finished = true - } - }) - }) - parser.on('end', finish) + const fileStream = await new Promise((resolve, reject) => { + multipart.reqParser(request.payload) + .on('file', (name, stream) => resolve(stream)) + .on('end', () => reject(Boom.badRequest("File argument 'data' is required"))) + }) - function finish () { - if (!finished) { - return setTimeout(finish, 10) - } - if (!file) { - return reply("File argument 'data' is required").code(400).takeover() - } + const data = await new Promise((resolve, reject) => { + fileStream + .on('data', data => resolve(data)) + .on('end', () => reject(Boom.badRequest("File argument 'data' is required"))) + }) + if (enc === 'protobuf') { try { - return reply({ - node: JSON.parse(file.toString()) - }) + return { node: await deserialize(data) } } catch (err) { - return reply({ - Message: 'Failed to parse the JSON: ' + err, - Code: 0 - }).code(500).takeover() + throw Boom.badRequest('Failed to deserialize: ' + err) } } + + let nodeJson + try { + nodeJson = JSON.parse(data.toString()) + } catch (err) { + throw Boom.badRequest('Failed to parse the JSON: ' + err) + } + + try { + return { node: await createDagNode(nodeJson.Data, nodeJson.Links) } + } catch (err) { + throw Boom.badRequest('Failed to create DAG node: ' + err) + } }, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const ipfs = request.server.app.ipfs - let node = request.pre.args.node - - waterfall([ - (cb) => DAGNode.create(Buffer.from(node.Data), node.Links, cb), - (node, cb) => ipfs.object.put(node, (err, cid) => cb(err, { cid, node })) - ], (err, results) => { - if (err) { - log.error(err) - - return reply({ - Message: 'Failed to put object: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { node } = request.pre.args - const nodeJSON = results.node.toJSON() - - const answer = { - Data: nodeJSON.data, - Hash: cidToString(results.cid, { base: request.query['cid-base'], upgrade: false }), - Size: nodeJSON.size, - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) - } + let cid + try { + cid = await ipfs.object.put(node) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to put node' }) + } - return reply(answer) - }) + const nodeJSON = node.toJSON() + + const answer = { + Data: nodeJSON.data, + Hash: cidToString(cid, { base: request.query['cid-base'], upgrade: false }), + Size: nodeJSON.size, + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } + }) + } + + return h.response(answer) } } @@ -265,23 +207,20 @@ exports.stat = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const ipfs = request.server.app.ipfs - const key = request.pre.args.key - - ipfs.object.stat(key, (err, stats) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to stat object: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { key } = request.pre.args - stats.Hash = cidToString(stats.Hash, { base: request.query['cid-base'], upgrade: false }) + let stats + try { + stats = await ipfs.object.stat(key) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to stat object' }) + } - return reply(stats) - }) + stats.Hash = cidToString(stats.Hash, { base: request.query['cid-base'], upgrade: false }) + + return h.response(stats) } } @@ -290,21 +229,18 @@ exports.data = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const ipfs = request.server.app.ipfs - const key = request.pre.args.key - - ipfs.object.data(key, (err, data) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get object data: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { key } = request.pre.args - return reply(data) - }) + let data + try { + data = await ipfs.object.data(key) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get object data' }) + } + + return h.response(data) } } @@ -319,72 +255,63 @@ exports.links = { parseArgs: exports.parseKey, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key - const ipfs = request.server.app.ipfs - - ipfs.object.get(key, (err, node) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to get object links: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { key } = request.pre.args + + let node + try { + node = await ipfs.object.get(key) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to get object links' }) + } + + const nodeJSON = node.toJSON() - const nodeJSON = node.toJSON() - - return reply({ - Hash: cidToString(key, { base: request.query['cid-base'], upgrade: false }), - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) + return h.response({ + Hash: cidToString(key, { base: request.query['cid-base'], upgrade: false }), + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } }) }) } } // common pre request handler that parses the args and returns `data` & `key` which are assigned to `request.pre.args` -exports.parseKeyAndData = (request, reply) => { +exports.parseKeyAndData = async (request, h) => { if (!request.query.arg) { - return reply("Argument 'root' is required").code(400).takeover() + throw Boom.badRequest("Argument 'root' is required") } if (!request.payload) { - return reply("File argument 'data' is required").code(400).takeover() + throw Boom.badRequest("File argument 'data' is required") } - const parser = multipart.reqParser(request.payload) - let file + // TODO: support ipfs paths: https://github.com/ipfs/http-api-spec/pull/68/files#diff-2625016b50d68d922257f74801cac29cR3880 + let cid + try { + cid = new CID(request.query.arg) + } catch (err) { + throw Boom.badRequest('invalid ipfs ref path') + } - parser.on('file', (fileName, fileStream) => { - fileStream.on('data', (data) => { - file = data - }) + const fileStream = await new Promise((resolve, reject) => { + multipart.reqParser(request.payload) + .on('file', (fileName, fileStream) => resolve(fileStream)) + .on('end', () => reject(Boom.badRequest("File argument 'data' is required"))) }) - parser.on('end', () => { - if (!file) { - return reply("File argument 'data' is required").code(400).takeover() - } - - try { - return reply({ - data: file, - // TODO: support ipfs paths: https://github.com/ipfs/http-api-spec/pull/68/files#diff-2625016b50d68d922257f74801cac29cR3880 - key: new CID(request.query.arg) - }) - } catch (err) { - return reply({ - Message: 'invalid ipfs ref path', - Code: 0 - }).code(500).takeover() - } + const fileData = await new Promise((resolve, reject) => { + fileStream + .on('data', data => resolve(data)) + .on('end', () => reject(Boom.badRequest("File argument 'data' is required"))) }) + + return { data: fileData, key: cid } } exports.patchAppendData = { @@ -398,41 +325,34 @@ exports.patchAppendData = { parseArgs: exports.parseKeyAndData, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key - const data = request.pre.args.data - const ipfs = request.server.app.ipfs - - waterfall([ - (cb) => ipfs.object.patch.appendData(key, data, cb), - (cid, cb) => ipfs.object.get(cid, (err, node) => cb(err, { node, cid })) - ], (err, results) => { - if (err) { - log.error(err) - - return reply({ - Message: 'Failed to append data to object: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { key, data } = request.pre.args - const nodeJSON = results.node.toJSON() - - const answer = { - Data: nodeJSON.data, - Hash: cidToString(results.cid, { base: request.query['cid-base'], upgrade: false }), - Size: nodeJSON.size, - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) - } + let cid, node + try { + cid = await ipfs.object.patch.appendData(key, data) + node = await ipfs.object.get(cid) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to append data to object' }) + } - return reply(answer) - }) + const nodeJSON = node.toJSON() + + const answer = { + Data: nodeJSON.data, + Hash: cidToString(cid, { base: request.query['cid-base'], upgrade: false }), + Size: nodeJSON.size, + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } + }) + } + + return h.response(answer) } } @@ -447,35 +367,28 @@ exports.patchSetData = { parseArgs: exports.parseKeyAndData, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const key = request.pre.args.key - const data = request.pre.args.data - const ipfs = request.server.app.ipfs - - waterfall([ - (cb) => ipfs.object.patch.setData(key, data, cb), - (cid, cb) => ipfs.object.get(cid, (err, node) => cb(err, { node, cid })) - ], (err, results) => { - if (err) { - log.error(err) - - return reply({ - Message: 'Failed to set data on object: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { key, data } = request.pre.args - const nodeJSON = results.node.toJSON() - - return reply({ - Hash: cidToString(results.cid, { base: request.query['cid-base'], upgrade: false }), - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) + let cid, node + try { + cid = await ipfs.object.patch.setData(key, data) + node = await ipfs.object.get(cid) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to set data on object' }) + } + + const nodeJSON = node.toJSON() + + return h.response({ + Hash: cidToString(cid, { base: request.query['cid-base'], upgrade: false }), + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } }) }) } @@ -492,75 +405,63 @@ exports.patchAddLink = { parseArgs: (request, reply) => { if (!(request.query.arg instanceof Array) || request.query.arg.length !== 3) { - return reply("Arguments 'root', 'name' & 'ref' are required").code(400).takeover() + throw Boom.badRequest("Arguments 'root', 'name' & 'ref' are required") } - const error = (msg) => reply({ - Message: msg, - Code: 0 - }).code(500).takeover() - if (!request.query.arg[0]) { - return error('cannot create link with no root') + throw Boom.badRequest('cannot create link with no root') } if (!request.query.arg[1]) { - return error('cannot create link with no name!') + throw Boom.badRequest('cannot create link with no name!') } if (!request.query.arg[2]) { - return error('cannot create link with no ref') + throw Boom.badRequest('cannot create link with no ref') } try { - return reply({ + return { root: new CID(request.query.arg[0]), name: request.query.arg[1], ref: new CID(request.query.arg[2]) - }) + } } catch (err) { log.error(err) - return error('invalid ipfs ref path') + throw Boom.badRequest('invalid ipfs ref path') } }, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const root = request.pre.args.root - const name = request.pre.args.name - const ref = request.pre.args.ref - const ipfs = request.server.app.ipfs - - waterfall([ - (cb) => ipfs.object.get(ref, cb), - (node, cb) => ipfs.object.patch.addLink(root, new DAGLink(name, node.size, ref), cb), - (cid, cb) => ipfs.object.get(cid, (err, node) => cb(err, { node, cid })) - ], (err, results) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to add link to object: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { root, name, ref } = request.pre.args - const nodeJSON = results.node.toJSON() - - const answer = { - Data: nodeJSON.data, - Hash: cidToString(results.cid, { base: request.query['cid-base'], upgrade: false }), - Size: nodeJSON.size, - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) - } + let node, cid + try { + node = await ipfs.object.get(ref) + cid = await ipfs.object.patch.addLink(root, new DAGLink(name, node.size, ref)) + node = await ipfs.object.get(cid) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to add link to object' }) + } - return reply(answer) - }) + const nodeJSON = node.toJSON() + + const answer = { + Data: nodeJSON.data, + Hash: cidToString(cid, { base: request.query['cid-base'], upgrade: false }), + Size: nodeJSON.size, + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } + }) + } + + return h.response(answer) } } @@ -572,67 +473,55 @@ exports.patchRmLink = { }, // pre request handler that parses the args and returns `root` & `link` which is assigned to `request.pre.args` - parseArgs: (request, reply) => { + parseArgs (request, h) { if (!(request.query.arg instanceof Array) || request.query.arg.length !== 2) { - return reply("Arguments 'root' & 'link' are required").code(400).takeover() + throw Boom.badRequest("Arguments 'root' & 'link' are required") } if (!request.query.arg[1]) { - return reply({ - Message: 'cannot remove link with no name!', - Code: 0 - }).code(500).takeover() + throw Boom.badRequest('cannot remove link with no name!') } try { - return reply({ + return { root: new CID(request.query.arg[0]), link: request.query.arg[1] - }) + } } catch (err) { log.error(err) - return reply({ - Message: 'invalid ipfs ref path', - Code: 0 - }).code(500).takeover() + throw Boom.badRequest('invalid ipfs ref path') } }, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const root = request.pre.args.root - const link = request.pre.args.link - const ipfs = request.server.app.ipfs - - waterfall([ - (cb) => ipfs.object.patch.rmLink(root, { name: link }, cb), - (cid, cb) => ipfs.object.get(cid, (err, node) => cb(err, { node, cid })) - ], (err, results) => { - if (err) { - log.error(err) - return reply({ - Message: 'Failed to remove link from object: ' + err, - Code: 0 - }).code(500) - } + async handler (request, h) { + const { ipfs } = request.server.app + const { root, link } = request.pre.args - const nodeJSON = results.node.toJSON() - - const answer = { - Data: nodeJSON.data, - Hash: cidToString(results.cid, { base: request.query['cid-base'], upgrade: false }), - Size: nodeJSON.size, - Links: nodeJSON.links.map((l) => { - return { - Name: l.name, - Size: l.size, - Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) - } - }) - } + let cid, node + try { + cid = await ipfs.object.patch.rmLink(root, { name: link }) + node = await ipfs.object.get(cid) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to remove link from object' }) + } - return reply(answer) - }) + const nodeJSON = node.toJSON() + + const answer = { + Data: nodeJSON.data, + Hash: cidToString(cid, { base: request.query['cid-base'], upgrade: false }), + Size: nodeJSON.size, + Links: nodeJSON.links.map((l) => { + return { + Name: l.name, + Size: l.size, + Hash: cidToString(l.cid, { base: request.query['cid-base'], upgrade: false }) + } + }) + } + + return h.response(answer) } } diff --git a/src/http/api/resources/pin.js b/src/http/api/resources/pin.js index 4aea71de58..b4232c6916 100644 --- a/src/http/api/resources/pin.js +++ b/src/http/api/resources/pin.js @@ -4,27 +4,27 @@ const mapValues = require('lodash/mapValues') const keyBy = require('lodash/keyBy') const multibase = require('multibase') const Joi = require('joi') +const Boom = require('boom') +const isIpfs = require('is-ipfs') const { cidToString } = require('../../../utils/cid') -const debug = require('debug') -const log = debug('jsipfs:http-api:pin') -log.error = debug('jsipfs:http-api:pin:error') - -exports = module.exports - -function parseArgs (request, reply) { - if (!request.query.arg) { - return reply({ - Message: "Argument 'arg' is required", - Code: 0 - }).code(400).takeover() + +function parseArgs (request, h) { + let { arg } = request.query + + if (!arg) { + throw Boom.badRequest("Argument 'arg' is required") } - const recursive = request.query.recursive !== 'false' + arg = Array.isArray(arg) ? arg : [arg] - return reply({ - path: request.query.arg, - recursive: recursive + arg.forEach(path => { + if (!isIpfs.ipfsPath(path) && !isIpfs.cid(path)) { + throw Boom.badRequest('invalid ipfs ref path') + } }) + + const recursive = request.query.recursive !== 'false' + return { path: arg, recursive } } exports.ls = { @@ -34,34 +34,39 @@ exports.ls = { }).unknown() }, - parseArgs: (request, reply) => { - const type = request.query.type || 'all' + parseArgs (request, h) { + let { arg } = request.query - return reply({ - path: request.query.arg, - type: type - }) + if (arg) { + arg = Array.isArray(arg) ? arg : [arg] + + arg.forEach(path => { + if (!isIpfs.ipfsPath(path) && !isIpfs.cid(path)) { + throw Boom.badRequest('invalid ipfs ref path') + } + }) + } + + const type = request.query.type || 'all' + return { path: request.query.arg, type } }, - handler: (request, reply) => { + async handler (request, h) { + const { ipfs } = request.server.app const { path, type } = request.pre.args - const ipfs = request.server.app.ipfs - - ipfs.pin.ls(path, { type }, (err, result) => { - if (err) { - log.error(err) - return reply({ - Message: `Failed to list pins: ${err.message}`, - Code: 0 - }).code(500) - } - return reply({ - Keys: mapValues( - keyBy(result, obj => cidToString(obj.hash, { base: request.query['cid-base'] })), - obj => ({ Type: obj.type }) - ) - }) + let result + try { + result = await ipfs.pin.ls(path, { type }) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to list pins' }) + } + + return h.response({ + Keys: mapValues( + keyBy(result, obj => cidToString(obj.hash, { base: request.query['cid-base'] })), + obj => ({ Type: obj.type }) + ) }) } } @@ -73,24 +78,24 @@ exports.add = { }).unknown() }, - parseArgs: parseArgs, + parseArgs, - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const { path, recursive } = request.pre.args - ipfs.pin.add(path, { recursive }, (err, result) => { - if (err) { - log.error(err) - return reply({ - Message: `Failed to add pin: ${err.message}`, - Code: 0 - }).code(500) + let result + try { + result = await ipfs.pin.add(path, { recursive }) + } catch (err) { + if (err.message.includes('already pinned recursively')) { + throw Boom.boomify(err, { statusCode: 400 }) } + throw Boom.boomify(err, { message: 'Failed to add pin' }) + } - return reply({ - Pins: result.map(obj => cidToString(obj.hash, { base: request.query['cid-base'] })) - }) + return h.response({ + Pins: result.map(obj => cidToString(obj.hash, { base: request.query['cid-base'] })) }) } } @@ -102,24 +107,21 @@ exports.rm = { }).unknown() }, - parseArgs: parseArgs, + parseArgs, - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const { path, recursive } = request.pre.args - ipfs.pin.rm(path, { recursive }, (err, result) => { - if (err) { - log.error(err) - return reply({ - Message: `Failed to remove pin: ${err.message}`, - Code: 0 - }).code(500) - } + let result + try { + result = await ipfs.pin.rm(path, { recursive }) + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to remove pin' }) + } - return reply({ - Pins: result.map(obj => cidToString(obj.hash, { base: request.query['cid-base'] })) - }) + return h.response({ + Pins: result.map(obj => cidToString(obj.hash, { base: request.query['cid-base'] })) }) } } diff --git a/src/http/api/resources/ping.js b/src/http/api/resources/ping.js index 56b54b3c05..01e8bc3bbc 100644 --- a/src/http/api/resources/ping.js +++ b/src/http/api/resources/ping.js @@ -2,12 +2,10 @@ const Joi = require('joi') const pull = require('pull-stream') -const toStream = require('pull-stream-to-stream') const ndjson = require('pull-ndjson') -const PassThrough = require('readable-stream').PassThrough -const pump = require('pump') +const { PassThrough } = require('readable-stream') -exports.get = { +module.exports = { validate: { query: Joi.object().keys({ n: Joi.alternatives() @@ -20,47 +18,36 @@ exports.get = { arg: Joi.string().required() }).unknown() }, - handler: (request, reply) => { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const peerId = request.query.arg // Default count to 10 const count = request.query.n || request.query.count || 10 - // Streams from pull-stream-to-stream don't seem to be compatible - // with the stream2 readable interface - // see: https://github.com/hapijs/hapi/blob/c23070a3de1b328876d5e64e679a147fafb04b38/lib/response.js#L533 - // and: https://github.com/pull-stream/pull-stream-to-stream/blob/e436acee18b71af8e71d1b5d32eee642351517c7/index.js#L28 - - const source = pull( - ipfs.pingPullStream(peerId, { count: count }), - pull.map((chunk) => ({ - Success: chunk.success, - Time: chunk.time, - Text: chunk.text - })), - ndjson.serialize() - ) - - const responseStream = toStream.source(source) - const stream2 = new PassThrough() - let replied = false - pump(responseStream, stream2) - - // FIXME: This is buffering all ping responses before sending them so that - // we can capture the error if it happens. #fixTheHTTPAPI - responseStream.on('error', (err) => { - if (!replied) { - replied = true - reply(err) - } + const responseStream = await new Promise((resolve, reject) => { + const stream = new PassThrough() + + pull( + ipfs.pingPullStream(peerId, { count }), + pull.map((chunk) => ({ + Success: chunk.success, + Time: chunk.time, + Text: chunk.text + })), + ndjson.serialize(), + pull.drain(chunk => { + stream.write(chunk) + }, err => { + if (err) return reject(err) + resolve(stream) + stream.end() + }) + ) }) - responseStream.on('end', () => { - if (!replied) { - replied = true - reply(stream2).type('application/json').header('X-Chunked-Output', '1') - } - }) + return h.response(responseStream) + .type('application/json') + .header('X-Chunked-Output', '1') } } diff --git a/src/http/api/resources/pubsub.js b/src/http/api/resources/pubsub.js index 82d8daabe4..9d217afd6b 100644 --- a/src/http/api/resources/pubsub.js +++ b/src/http/api/resources/pubsub.js @@ -3,20 +3,19 @@ const PassThrough = require('stream').PassThrough const bs58 = require('bs58') const binaryQueryString = require('binary-querystring') - -exports = module.exports +const Boom = require('boom') exports.subscribe = { - handler: (request, reply) => { + async handler (request, h) { const query = request.query const discover = query.discover === 'true' const topic = query.arg if (!topic) { - return reply(new Error('Missing topic')) + throw Boom.badRequest('Missing topic') } - const ipfs = request.server.app.ipfs + const { ipfs } = request.server.app const res = new PassThrough({ highWaterMark: 1 }) @@ -36,79 +35,77 @@ exports.subscribe = { ipfs.pubsub.unsubscribe(topic, handler, () => res.end()) } - request.once('disconnect', unsubscribe) - request.once('finish', unsubscribe) + request.events.once('disconnect', unsubscribe) + request.events.once('finish', unsubscribe) - ipfs.pubsub.subscribe(topic, handler, { discover: discover }, (err) => { - if (err) { - return reply(err) - } + await ipfs.pubsub.subscribe(topic, handler, { discover: discover }) - reply(res) - .header('X-Chunked-Output', '1') - .header('content-encoding', 'identity') // stop gzip from buffering, see https://github.com/hapijs/hapi/issues/2975 - .header('content-type', 'application/json') - }) + return h.response(res) + .header('X-Chunked-Output', '1') + .header('content-encoding', 'identity') // stop gzip from buffering, see https://github.com/hapijs/hapi/issues/2975 + .header('content-type', 'application/json') } } exports.publish = { - handler: (request, reply) => { - const arg = request.query.arg + async handler (request, h) { + const { arg } = request.query const topic = arg[0] const rawArgs = binaryQueryString(request.url.search) const buf = rawArgs.arg && rawArgs.arg[1] - const ipfs = request.server.app.ipfs + const { ipfs } = request.server.app if (!topic) { - return reply(new Error('Missing topic')) + throw Boom.badRequest('Missing topic') } if (!buf || buf.length === 0) { - return reply(new Error('Missing buf')) + throw Boom.badRequest('Missing buf') } - ipfs.pubsub.publish(topic, buf, (err) => { - if (err) { - return reply(new Error(`Failed to publish to topic ${topic}: ${err}`)) - } + try { + await ipfs.pubsub.publish(topic, buf) + } catch (err) { + throw Boom.boomify(err, { message: `Failed to publish to topic ${topic}` }) + } - reply() - }) + return h.response() } } exports.ls = { - handler: (request, reply) => { - const ipfs = request.server.app.ipfs - - ipfs.pubsub.ls((err, subscriptions) => { - if (err) { - return reply(new Error(`Failed to list subscriptions: ${err}`)) - } + async handler (request, h) { + const { ipfs } = request.server.app + + let subscriptions + try { + subscriptions = await ipfs.pubsub.ls() + } catch (err) { + throw Boom.boomify(err, { message: 'Failed to list subscriptions' }) + } - reply({ Strings: subscriptions }) - }) + return h.response({ Strings: subscriptions }) } } exports.peers = { - handler: (request, reply) => { + async handler (request, h) { const topic = request.query.arg - const ipfs = request.server.app.ipfs + const { ipfs } = request.server.app - ipfs.pubsub.peers(topic, (err, peers) => { - if (err) { - const message = topic - ? `Failed to find peers subscribed to ${topic}: ${err}` - : `Failed to find peers: ${err}` + let peers + try { + peers = await ipfs.pubsub.peers(topic) + } catch (err) { + const message = topic + ? `Failed to find peers subscribed to ${topic}: ${err}` + : `Failed to find peers: ${err}` - return reply(new Error(message)) - } + throw Boom.boomify(err, { message }) + } - reply({ Strings: peers }) - }) + return h.response({ Strings: peers }) } } diff --git a/src/http/api/resources/repo.js b/src/http/api/resources/repo.js index 13b68e115a..998431111a 100644 --- a/src/http/api/resources/repo.js +++ b/src/http/api/resources/repo.js @@ -1,57 +1,29 @@ 'use strict' -exports = module.exports - -exports.gc = (request, reply) => { - const ipfs = request.server.app.ipfs - - ipfs.repo.gc((err) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - reply() - }) +exports.gc = async (request, h) => { + const { ipfs } = request.server.app + await ipfs.repo.gc() + return h.response() } -exports.version = (request, reply) => { - const ipfs = request.server.app.ipfs - - ipfs.repo.version((err, version) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - reply({ - Version: version - }) +exports.version = async (request, h) => { + const { ipfs } = request.server.app + const version = await ipfs.repo.version() + return h.response({ + Version: version }) } -exports.stat = (request, reply) => { - const ipfs = request.server.app.ipfs +exports.stat = async (request, h) => { + const { ipfs } = request.server.app const human = request.query.human === 'true' - - ipfs.repo.stat({ human: human }, (err, stat) => { - if (err) { - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - reply({ - NumObjects: stat.numObjects, - RepoSize: stat.repoSize, - RepoPath: stat.repoPath, - Version: stat.version, - StorageMax: stat.storageMax - }) + const stat = await ipfs.repo.stat({ human }) + + return h.response({ + NumObjects: stat.numObjects, + RepoSize: stat.repoSize, + RepoPath: stat.repoPath, + Version: stat.version, + StorageMax: stat.storageMax }) } diff --git a/src/http/api/resources/resolve.js b/src/http/api/resources/resolve.js index 478e423595..0984aeb5a6 100644 --- a/src/http/api/resources/resolve.js +++ b/src/http/api/resources/resolve.js @@ -21,20 +21,15 @@ module.exports = { 'cid-base': Joi.string().valid(multibase.names) }).unknown() }, - handler (request, reply) { - const ipfs = request.server.app.ipfs + async handler (request, h) { + const { ipfs } = request.server.app const name = request.query.arg const recursive = request.query.r || request.query.recursive || false const cidBase = request.query['cid-base'] log(name, { recursive, cidBase }) + const res = await ipfs.resolve(name, { recursive, cidBase }) - ipfs.resolve(name, { recursive, cidBase }, (err, res) => { - if (err) { - log.error(err) - return reply({ Message: err.message, Code: 0 }).code(500) - } - reply({ Path: res }) - }) + return h.response({ Path: res }) } } diff --git a/src/http/api/resources/shutdown.js b/src/http/api/resources/shutdown.js index 56f137cc91..90eb66790f 100644 --- a/src/http/api/resources/shutdown.js +++ b/src/http/api/resources/shutdown.js @@ -1,14 +1,12 @@ 'use strict' -exports = module.exports - /* * Stop the daemon. * * Returns an empty response to the caller then * on the next 'tick' emits SIGTERM. */ -exports.do = (request, reply) => { +module.exports = (request, h) => { setImmediate(() => process.emit('SIGTERM')) - return reply() + return h.response() } diff --git a/src/http/api/resources/stats.js b/src/http/api/resources/stats.js index 7ec7b0cb42..616b678154 100644 --- a/src/http/api/resources/stats.js +++ b/src/http/api/resources/stats.js @@ -11,14 +11,12 @@ const transformBandwidth = (stat) => { } } -exports = module.exports - exports.bitswap = require('./bitswap').stat exports.repo = require('./repo').stat -exports.bw = (request, reply) => { - const ipfs = request.server.app.ipfs +exports.bw = async (request, h) => { + const { ipfs } = request.server.app const options = { peer: request.query.peer, proto: request.query.proto, @@ -35,12 +33,13 @@ exports.bw = (request, reply) => { } }) - request.on('disconnect', () => { + request.events.on('disconnect', () => { res.destroy() }) res.pipe(output) - reply(output) + + return h.response(output) .header('content-type', 'application/json') .header('x-chunked-output', '1') } diff --git a/src/http/api/resources/swarm.js b/src/http/api/resources/swarm.js index 918f7c8ba3..7487c4d503 100644 --- a/src/http/api/resources/swarm.js +++ b/src/http/api/resources/swarm.js @@ -1,111 +1,74 @@ 'use strict' -const debug = require('debug') -const log = debug('jsipfs:http-api:block') -log.error = debug('jsipfs:http-api:block:error') const multiaddr = require('multiaddr') - -exports = module.exports +const Boom = require('boom') // common pre request handler that parses the args and returns `addr` which is assigned to `request.pre.args` -exports.parseAddrs = (request, reply) => { +exports.parseAddrs = (request, h) => { if (!request.query.arg) { - const err = 'Argument `addr` is required' - log.error(err) - return reply({ - Code: 0, - Message: err - }).code(400).takeover() + throw Boom.badRequest('Argument `addr` is required') } try { multiaddr(request.query.arg) } catch (err) { - log.error(err) - return reply({ - Code: 0, - Message: err.message - }).code(500).takeover() + throw Boom.boomify(err, { statusCode: 400 }) } - return reply({ + return { addr: request.query.arg - }) + } } exports.peers = { - handler: (request, reply) => { + async handler (request, h) { const rawVerbose = request.query.v || request.query.verbose const verbose = rawVerbose === 'true' - const ipfs = request.server.app.ipfs - - ipfs.swarm.peers({ verbose: verbose }, (err, peers) => { - if (err) { - log.error(err) - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - return reply({ - Peers: peers.map((p) => { - const res = { - Peer: p.peer.toB58String(), - Addr: p.addr.toString() - } - - if (verbose) { - res.Latency = p.latency - } - - return res - }) + const { ipfs } = request.server.app + + const peers = await ipfs.swarm.peers({ verbose }) + + return h.response({ + Peers: peers.map((p) => { + const res = { + Peer: p.peer.toB58String(), + Addr: p.addr.toString() + } + + if (verbose) { + res.Latency = p.latency + } + + return res }) }) } } exports.addrs = { - handler: (request, reply) => { - const ipfs = request.server.app.ipfs - ipfs.swarm.addrs((err, peers) => { - if (err) { - log.error(err) - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - const addrs = {} - peers.forEach((peer) => { - addrs[peer.id.toB58String()] = peer.multiaddrs.toArray() - .map((addr) => addr.toString()) - }) + async handler (request, h) { + const { ipfs } = request.server.app + const peers = await ipfs.swarm.addrs() + + const addrs = {} + peers.forEach((peer) => { + addrs[peer.id.toB58String()] = peer.multiaddrs.toArray() + .map((addr) => addr.toString()) + }) - return reply({ - Addrs: addrs - }) + return h.response({ + Addrs: addrs }) } } exports.localAddrs = { - handler: (request, reply) => { - const ipfs = request.server.app.ipfs - ipfs.swarm.localAddrs((err, addrs) => { - if (err) { - log.error(err) - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - return reply({ - Strings: addrs.map((addr) => addr.toString()) - }) + async handler (request, h) { + const { ipfs } = request.server.app + const addrs = await ipfs.swarm.localAddrs() + + return h.response({ + Strings: addrs.map((addr) => addr.toString()) }) } } @@ -115,22 +78,14 @@ exports.connect = { parseArgs: exports.parseAddrs, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const addr = request.pre.args.addr - const ipfs = request.server.app.ipfs - - ipfs.swarm.connect(addr, (err) => { - if (err) { - log.error(err) - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - reply({ - Strings: [`connect ${addr} success`] - }) + async handler (request, h) { + const { addr } = request.pre.args + const { ipfs } = request.server.app + + await ipfs.swarm.connect(addr) + + return h.response({ + Strings: [`connect ${addr} success`] }) } } @@ -140,22 +95,14 @@ exports.disconnect = { parseArgs: exports.parseAddrs, // main route handler which is called after the above `parseArgs`, but only if the args were valid - handler: (request, reply) => { - const addr = request.pre.args.addr - const ipfs = request.server.app.ipfs - - ipfs.swarm.disconnect(addr, (err) => { - if (err) { - log.error(err) - return reply({ - Message: err.toString(), - Code: 0 - }).code(500) - } - - return reply({ - Strings: [`disconnect ${addr} success`] - }) + async handler (request, h) { + const { addr } = request.pre.args + const { ipfs } = request.server.app + + await ipfs.swarm.disconnect(addr) + + return h.response({ + Strings: [`disconnect ${addr} success`] }) } } diff --git a/src/http/api/resources/version.js b/src/http/api/resources/version.js index 6d66211cb4..833f0caecb 100644 --- a/src/http/api/resources/version.js +++ b/src/http/api/resources/version.js @@ -1,21 +1,12 @@ 'use strict' -const boom = require('boom') - -exports = module.exports - -exports.get = (request, reply) => { - const ipfs = request.server.app.ipfs - - ipfs.version((err, version) => { - if (err) { - return reply(boom.badRequest(err)) - } - - reply({ - Version: version.version, - Commit: version.commit, - Repo: version.repo - }) +module.exports = async (request, h) => { + const { ipfs } = request.server.app + const version = await ipfs.version() + + return h.response({ + Version: version.version, + Commit: version.commit, + Repo: version.repo }) } diff --git a/src/http/api/routes/bitswap.js b/src/http/api/routes/bitswap.js index 45be830b6d..8a59fe6169 100644 --- a/src/http/api/routes/bitswap.js +++ b/src/http/api/routes/bitswap.js @@ -1,37 +1,33 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/bitswap/wantlist', - config: { - handler: resources.bitswap.wantlist.handler, + options: { validate: resources.bitswap.wantlist.validate - } - }) - - api.route({ + }, + handler: resources.bitswap.wantlist.handler + }, + { method: '*', path: '/api/v0/bitswap/stat', - config: { - handler: resources.bitswap.stat.handler, + options: { validate: resources.bitswap.stat.validate - } - }) - - api.route({ + }, + handler: resources.bitswap.stat.handler + }, + { method: '*', path: '/api/v0/bitswap/unwant', - config: { + options: { pre: [ { method: resources.bitswap.unwant.parseArgs, assign: 'args' } ], - handler: resources.bitswap.unwant.handler, validate: resources.bitswap.unwant.validate - } - }) -} + }, + handler: resources.bitswap.unwant.handler + } +] diff --git a/src/http/api/routes/block.js b/src/http/api/routes/block.js index 03d8159876..1e25b98543 100644 --- a/src/http/api/routes/block.js +++ b/src/http/api/routes/block.js @@ -1,25 +1,22 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/block/get', - config: { + options: { pre: [ { method: resources.block.get.parseArgs, assign: 'args' } - ], - handler: resources.block.get.handler - } - }) - - api.route({ + ] + }, + handler: resources.block.get.handler + }, + { method: '*', path: '/api/v0/block/put', - config: { + options: { payload: { parse: false, output: 'stream' @@ -27,31 +24,29 @@ module.exports = (server) => { pre: [ { method: resources.block.put.parseArgs, assign: 'args' } ], - handler: resources.block.put.handler, validate: resources.block.put.validate - } - }) - - api.route({ + }, + handler: resources.block.put.handler + }, + { method: '*', path: '/api/v0/block/rm', config: { pre: [ { method: resources.block.rm.parseArgs, assign: 'args' } - ], - handler: resources.block.rm.handler - } - }) - - api.route({ + ] + }, + handler: resources.block.rm.handler + }, + { method: '*', path: '/api/v0/block/stat', config: { pre: [ { method: resources.block.stat.parseArgs, assign: 'args' } ], - handler: resources.block.stat.handler, validate: resources.block.stat.validate - } - }) -} + }, + handler: resources.block.stat.handler + } +] diff --git a/src/http/api/routes/bootstrap.js b/src/http/api/routes/bootstrap.js index dcc512a952..ec37966895 100644 --- a/src/http/api/routes/bootstrap.js +++ b/src/http/api/routes/bootstrap.js @@ -1,47 +1,46 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - // https://github.com/ipfs/http-api-spec/blob/master/apiary.apib#L818 - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/bootstrap', - config: { - handler: resources.bootstrap.list - } - }) - - // https://github.com/ipfs/http-api-spec/blob/master/apiary.apib#L866 - api.route({ + handler: resources.bootstrap.list + }, + { method: '*', path: '/api/v0/bootstrap/add', - config: { + options: { pre: [ { method: resources.bootstrap.add.parseArgs, assign: 'args' } - ], - handler: resources.bootstrap.add.handler - } - }) - - // https://github.com/ipfs/http-api-spec/blob/master/apiary.apib#L1081 - api.route({ + ] + }, + handler: resources.bootstrap.add.handler + }, + { + method: '*', + path: '/api/v0/bootstrap/add/default', + handler: resources.bootstrap.addDefault + }, + { method: '*', path: '/api/v0/bootstrap/list', handler: resources.bootstrap.list - }) - - // https://github.com/ipfs/http-api-spec/blob/master/apiary.apib#L1131 - api.route({ + }, + { method: '*', path: '/api/v0/bootstrap/rm', - config: { + options: { pre: [ { method: resources.bootstrap.rm.parseArgs, assign: 'args' } - ], - handler: resources.bootstrap.rm.handler - } - }) -} + ] + }, + handler: resources.bootstrap.rm.handler + }, + { + method: '*', + path: '/api/v0/bootstrap/rm/all', + handler: resources.bootstrap.rmAll + } +] diff --git a/src/http/api/routes/config.js b/src/http/api/routes/config.js index 452d84ed0b..5315a043dc 100644 --- a/src/http/api/routes/config.js +++ b/src/http/api/routes/config.js @@ -1,39 +1,35 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/config/{key?}', - config: { + options: { pre: [ { method: resources.config.getOrSet.parseArgs, assign: 'args' } - ], - handler: resources.config.getOrSet.handler - } - }) - - api.route({ + ] + }, + handler: resources.config.getOrSet.handler + }, + { method: '*', path: '/api/v0/config/show', handler: resources.config.show - }) - - api.route({ + }, + { method: '*', path: '/api/v0/config/replace', - config: { + options: { payload: { parse: false, output: 'stream' }, pre: [ { method: resources.config.replace.parseArgs, assign: 'args' } - ], - handler: resources.config.replace.handler - } - }) -} + ] + }, + handler: resources.config.replace.handler + } +] diff --git a/src/http/api/routes/debug.js b/src/http/api/routes/debug.js index d5ae1e434d..d57989b323 100644 --- a/src/http/api/routes/debug.js +++ b/src/http/api/routes/debug.js @@ -1,31 +1,27 @@ 'use strict' -const register = require('prom-client').register const client = require('prom-client') +const Boom = require('boom') -// Endpoint for handling debug metrics -module.exports = (server) => { - const api = server.select('API') - // Clear the register to make sure we're not registering multiple ones - register.clear() - const gauge = new client.Gauge({ name: 'number_of_peers', help: 'the_number_of_currently_connected_peers' }) +// Clear the register to make sure we're not registering multiple ones +client.register.clear() +const gauge = new client.Gauge({ name: 'number_of_peers', help: 'the_number_of_currently_connected_peers' }) - api.route({ - method: 'GET', - path: '/debug/metrics/prometheus', - handler: (request, reply) => { - if (!process.env.IPFS_MONITORING) { - return reply('Monitoring is disabled. Enable it by setting environment variable IPFS_MONITORING') - .code(501) // 501 = Not Implemented - } - server.app.ipfs.swarm.peers((err, res) => { - if (err) { - return reply(err).code(500) - } - const count = res.length - gauge.set(count) - reply(register.metrics()).header('Content-Type', register.contentType) - }) +// Endpoint for handling debug metrics +module.exports = { + method: 'GET', + path: '/debug/metrics/prometheus', + async handler (request, h) { + if (!process.env.IPFS_MONITORING) { + throw Boom.notImplemented('Monitoring is disabled. Enable it by setting environment variable IPFS_MONITORING') } - }) + + const { ipfs } = request.server.app + const peers = await ipfs.swarm.peers() + + gauge.set(peers.length) + + return h.response(client.register.metrics()) + .type(client.register.contentType) + } } diff --git a/src/http/api/routes/dns.js b/src/http/api/routes/dns.js index dd3f074025..4a3bbec3c9 100644 --- a/src/http/api/routes/dns.js +++ b/src/http/api/routes/dns.js @@ -1,13 +1,9 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ - method: '*', - path: '/api/v0/dns', - handler: resources.dns.get - }) +module.exports = { + method: '*', + path: '/api/v0/dns', + handler: resources.dns } diff --git a/src/http/api/routes/file.js b/src/http/api/routes/file.js index 1abf7456b2..5fd57355bd 100644 --- a/src/http/api/routes/file.js +++ b/src/http/api/routes/file.js @@ -1,19 +1,15 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ - // TODO fix method - method: '*', - path: '/api/v0/file/ls', - config: { - pre: [ - { method: resources.file.ls.parseArgs, assign: 'args' } - ], - handler: resources.file.ls.handler - } - }) +module.exports = { + // TODO fix method + method: '*', + path: '/api/v0/file/ls', + config: { + pre: [ + { method: resources.file.ls.parseArgs, assign: 'args' } + ] + }, + handler: resources.file.ls.handler } diff --git a/src/http/api/routes/files.js b/src/http/api/routes/files-regular.js similarity index 56% rename from src/http/api/routes/files.js rename to src/http/api/routes/files-regular.js index 2e51ca7b0c..537116e38e 100644 --- a/src/http/api/routes/files.js +++ b/src/http/api/routes/files-regular.js @@ -1,61 +1,53 @@ 'use strict' -const resources = require('./../resources') -const mfs = require('ipfs-mfs/http') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { // TODO fix method method: '*', path: '/api/v0/cat', - config: { + options: { pre: [ { method: resources.filesRegular.cat.parseArgs, assign: 'args' } - ], - handler: resources.filesRegular.cat.handler - } - }) - - api.route({ + ] + }, + handler: resources.filesRegular.cat.handler + }, + { // TODO fix method method: '*', path: '/api/v0/get', - config: { + options: { pre: [ { method: resources.filesRegular.get.parseArgs, assign: 'args' } - ], - handler: resources.filesRegular.get.handler - } - }) - - api.route({ + ] + }, + handler: resources.filesRegular.get.handler + }, + { // TODO fix method method: '*', path: '/api/v0/add', - config: { + options: { payload: { parse: false, output: 'stream', maxBytes: Number.MAX_SAFE_INTEGER }, - handler: resources.filesRegular.add.handler, validate: resources.filesRegular.add.validate - } - }) - - api.route({ + }, + handler: resources.filesRegular.add.handler + }, + { // TODO fix method method: '*', path: '/api/v0/ls', - config: { + options: { pre: [ { method: resources.filesRegular.ls.parseArgs, assign: 'args' } - ], - handler: resources.filesRegular.ls.handler - } - }) - - mfs(api) -} + ] + }, + handler: resources.filesRegular.ls.handler + } +] diff --git a/src/http/api/routes/id.js b/src/http/api/routes/id.js index 9a13f72efc..3af3386301 100644 --- a/src/http/api/routes/id.js +++ b/src/http/api/routes/id.js @@ -1,13 +1,9 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ - method: '*', - path: '/api/v0/id', - handler: resources.id.get - }) +module.exports = { + method: '*', + path: '/api/v0/id', + handler: resources.id.get } diff --git a/src/http/api/routes/index.js b/src/http/api/routes/index.js index 450df4e4a5..8f310ee73e 100644 --- a/src/http/api/routes/index.js +++ b/src/http/api/routes/index.js @@ -1,26 +1,27 @@ 'use strict' -module.exports = (server) => { - require('./version')(server) - require('./shutdown')(server) - require('./id')(server) - require('./bootstrap')(server) - require('./block')(server) - require('./object')(server) - require('./pin')(server) - require('./repo')(server) - require('./config')(server) - require('./ping')(server) - require('./swarm')(server) - require('./bitswap')(server) - require('./file')(server) - require('./files')(server) - require('./pubsub')(server) - require('./debug')(server) - require('./webui')(server) - require('./dns')(server) - require('./key')(server) - require('./stats')(server) - require('./resolve')(server) - require('./name')(server) -} +module.exports = [ + require('./version'), + require('./shutdown'), + require('./id'), + ...require('./bootstrap'), + ...require('./block'), + ...require('./object'), + ...require('./pin'), + ...require('./repo'), + ...require('./config'), + require('./ping'), + ...require('./swarm'), + ...require('./bitswap'), + require('./file'), + ...require('./files-regular'), + ...require('ipfs-mfs/http'), + ...require('./pubsub'), + require('./debug'), + ...require('./webui'), + require('./dns'), + ...require('./key'), + ...require('./stats'), + require('./resolve'), + ...require('./name') +] diff --git a/src/http/api/routes/key.js b/src/http/api/routes/key.js index 4493bdab39..036efd4637 100644 --- a/src/http/api/routes/key.js +++ b/src/http/api/routes/key.js @@ -1,43 +1,36 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/key/list', handler: resources.key.list - }) - - api.route({ + }, + { method: '*', path: '/api/v0/key/gen', handler: resources.key.gen - }) - - api.route({ + }, + { method: '*', path: '/api/v0/key/rm', handler: resources.key.rm - }) - - api.route({ + }, + { method: '*', path: '/api/v0/key/rename', handler: resources.key.rename - }) - - api.route({ + }, + { method: '*', path: '/api/v0/key/export', handler: resources.key.export - }) - - api.route({ + }, + { method: '*', path: '/api/v0/key/import', handler: resources.key.import - }) -} + } +] diff --git a/src/http/api/routes/name.js b/src/http/api/routes/name.js index f49d0bd6ca..a2a7a61e4c 100644 --- a/src/http/api/routes/name.js +++ b/src/http/api/routes/name.js @@ -1,50 +1,40 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/name/resolve', - config: { - handler: resources.name.resolve.handler, + options: { validate: resources.name.resolve.validate - } - }) - - api.route({ + }, + handler: resources.name.resolve.handler + }, + { method: '*', path: '/api/v0/name/publish', - config: { - handler: resources.name.publish.handler, + options: { validate: resources.name.publish.validate - } - }) - - api.route({ + }, + handler: resources.name.publish.handler + }, + { method: '*', path: '/api/v0/name/pubsub/state', - config: { - handler: resources.name.pubsub.state.handler - } - }) - - api.route({ + handler: resources.name.pubsub.state.handler + }, + { method: '*', path: '/api/v0/name/pubsub/subs', - config: { - handler: resources.name.pubsub.subs.handler - } - }) - - api.route({ + handler: resources.name.pubsub.subs.handler + }, + { method: '*', path: '/api/v0/name/pubsub/cancel', - config: { - handler: resources.name.pubsub.cancel.handler, + options: { validate: resources.name.pubsub.cancel.validate - } - }) -} + }, + handler: resources.name.pubsub.cancel.handler + } +] diff --git a/src/http/api/routes/object.js b/src/http/api/routes/object.js index 407b6c58a0..c7c0daae4b 100644 --- a/src/http/api/routes/object.js +++ b/src/http/api/routes/object.js @@ -1,35 +1,31 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/object/new', - config: { - handler: resources.object.new.handler, + options: { validate: resources.object.new.validate - } - }) - - api.route({ + }, + handler: resources.object.new.handler + }, + { method: '*', path: '/api/v0/object/get', - config: { + options: { pre: [ { method: resources.object.get.parseArgs, assign: 'args' } ], - handler: resources.object.get.handler, validate: resources.object.get.validate - } - }) - - api.route({ + }, + handler: resources.object.get.handler + }, + { method: '*', path: '/api/v0/object/put', - config: { + options: { payload: { parse: false, output: 'stream' @@ -37,50 +33,46 @@ module.exports = (server) => { pre: [ { method: resources.object.put.parseArgs, assign: 'args' } ], - handler: resources.object.put.handler, validate: resources.object.put.validate - } - }) - - api.route({ + }, + handler: resources.object.put.handler + }, + { method: '*', path: '/api/v0/object/stat', - config: { + options: { pre: [ { method: resources.object.stat.parseArgs, assign: 'args' } ], - handler: resources.object.stat.handler, validate: resources.object.stat.validate - } - }) - - api.route({ + }, + handler: resources.object.stat.handler + }, + { method: '*', path: '/api/v0/object/data', - config: { + options: { pre: [ { method: resources.object.data.parseArgs, assign: 'args' } - ], - handler: resources.object.data.handler - } - }) - - api.route({ + ] + }, + handler: resources.object.data.handler + }, + { method: '*', path: '/api/v0/object/links', - config: { + options: { pre: [ { method: resources.object.links.parseArgs, assign: 'args' } ], - handler: resources.object.links.handler, validate: resources.object.links.validate - } - }) - - api.route({ + }, + handler: resources.object.links.handler + }, + { method: '*', path: '/api/v0/object/patch/append-data', - config: { + options: { payload: { parse: false, output: 'stream' @@ -88,15 +80,14 @@ module.exports = (server) => { pre: [ { method: resources.object.patchAppendData.parseArgs, assign: 'args' } ], - handler: resources.object.patchAppendData.handler, validate: resources.object.patchAppendData.validate - } - }) - - api.route({ + }, + handler: resources.object.patchAppendData.handler + }, + { method: '*', path: '/api/v0/object/patch/set-data', - config: { + options: { payload: { parse: false, output: 'stream' @@ -104,32 +95,30 @@ module.exports = (server) => { pre: [ { method: resources.object.patchSetData.parseArgs, assign: 'args' } ], - handler: resources.object.patchSetData.handler, validate: resources.object.patchSetData.validate - } - }) - - api.route({ + }, + handler: resources.object.patchSetData.handler + }, + { method: '*', path: '/api/v0/object/patch/add-link', - config: { + options: { pre: [ { method: resources.object.patchAddLink.parseArgs, assign: 'args' } ], - handler: resources.object.patchAddLink.handler, validate: resources.object.patchAddLink.validate - } - }) - - api.route({ + }, + handler: resources.object.patchAddLink.handler + }, + { method: '*', path: '/api/v0/object/patch/rm-link', - config: { + options: { pre: [ { method: resources.object.patchRmLink.parseArgs, assign: 'args' } ], - handler: resources.object.patchRmLink.handler, validate: resources.object.patchRmLink.validate - } - }) -} + }, + handler: resources.object.patchRmLink.handler + } +] diff --git a/src/http/api/routes/pin.js b/src/http/api/routes/pin.js index 76b6a57ad1..6912fe42fc 100644 --- a/src/http/api/routes/pin.js +++ b/src/http/api/routes/pin.js @@ -1,43 +1,39 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/pin/add', - config: { + options: { pre: [ { method: resources.pin.add.parseArgs, assign: 'args' } ], - handler: resources.pin.add.handler, validate: resources.pin.add.validate - } - }) - - api.route({ + }, + handler: resources.pin.add.handler + }, + { method: '*', path: '/api/v0/pin/rm', - config: { + options: { pre: [ { method: resources.pin.rm.parseArgs, assign: 'args' } ], - handler: resources.pin.rm.handler, validate: resources.pin.rm.validate - } - }) - - api.route({ + }, + handler: resources.pin.rm.handler + }, + { method: '*', path: '/api/v0/pin/ls', config: { pre: [ { method: resources.pin.ls.parseArgs, assign: 'args' } ], - handler: resources.pin.ls.handler, validate: resources.pin.ls.validate - } - }) -} + }, + handler: resources.pin.ls.handler + } +] diff --git a/src/http/api/routes/ping.js b/src/http/api/routes/ping.js index 92b081862f..a2eff08989 100644 --- a/src/http/api/routes/ping.js +++ b/src/http/api/routes/ping.js @@ -1,16 +1,12 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ - method: '*', - path: '/api/v0/ping', - config: { - handler: resources.ping.get.handler, - validate: resources.ping.get.validate - } - }) +module.exports = { + method: '*', + path: '/api/v0/ping', + config: { + handler: resources.ping.handler, + validate: resources.ping.validate + } } diff --git a/src/http/api/routes/pubsub.js b/src/http/api/routes/pubsub.js index 6f55b52b65..1f70ef892e 100644 --- a/src/http/api/routes/pubsub.js +++ b/src/http/api/routes/pubsub.js @@ -1,39 +1,26 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/pubsub/sub', - config: { - handler: resources.pubsub.subscribe.handler - } - }) - - api.route({ + handler: resources.pubsub.subscribe.handler + }, + { method: '*', path: '/api/v0/pubsub/pub', - config: { - handler: resources.pubsub.publish.handler - } - }) - - api.route({ + handler: resources.pubsub.publish.handler + }, + { method: '*', path: '/api/v0/pubsub/ls', - config: { - handler: resources.pubsub.ls.handler - } - }) - - api.route({ + handler: resources.pubsub.ls.handler + }, + { method: '*', path: '/api/v0/pubsub/peers', - config: { - handler: resources.pubsub.peers.handler - } - }) -} + handler: resources.pubsub.peers.handler + } +] diff --git a/src/http/api/routes/repo.js b/src/http/api/routes/repo.js index 3eb7ab2a88..21b306e51e 100644 --- a/src/http/api/routes/repo.js +++ b/src/http/api/routes/repo.js @@ -1,25 +1,17 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/repo/version', - config: { - handler: resources.repo.version - } - }) - - api.route({ + handler: resources.repo.version + }, + { method: '*', path: '/api/v0/repo/stat', - config: { - handler: resources.repo.stat - } - }) - + handler: resources.repo.stat + } // TODO: implement the missing spec https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/REPO.md -} +] diff --git a/src/http/api/routes/resolve.js b/src/http/api/routes/resolve.js index 259ae3bdd1..a95a2e2d62 100644 --- a/src/http/api/routes/resolve.js +++ b/src/http/api/routes/resolve.js @@ -1,16 +1,12 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ - method: '*', - path: '/api/v0/resolve', - config: { - handler: resources.resolve.handler, - validate: resources.resolve.validate - } - }) +module.exports = { + method: '*', + path: '/api/v0/resolve', + options: { + validate: resources.resolve.validate + }, + handler: resources.resolve.handler } diff --git a/src/http/api/routes/shutdown.js b/src/http/api/routes/shutdown.js index b3d8c9f8ef..f7201a40e6 100644 --- a/src/http/api/routes/shutdown.js +++ b/src/http/api/routes/shutdown.js @@ -1,13 +1,9 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ - method: '*', - path: '/api/v0/shutdown', - handler: resources.shutdown.do - }) +module.exports = { + method: '*', + path: '/api/v0/shutdown', + handler: resources.shutdown } diff --git a/src/http/api/routes/stats.js b/src/http/api/routes/stats.js index 1fe0707c1b..86b259c2f0 100644 --- a/src/http/api/routes/stats.js +++ b/src/http/api/routes/stats.js @@ -1,31 +1,24 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/stats/bitswap', - config: { - handler: resources.stats.bitswap.handler - } - }) - - api.route({ + options: { + validate: resources.stats.bitswap.validate + }, + handler: resources.stats.bitswap.handler + }, + { method: '*', path: '/api/v0/stats/repo', - config: { - handler: resources.stats.repo - } - }) - - api.route({ + handler: resources.stats.repo + }, + { method: '*', path: '/api/v0/stats/bw', - config: { - handler: resources.stats.bw - } - }) -} + handler: resources.stats.bw + } +] diff --git a/src/http/api/routes/swarm.js b/src/http/api/routes/swarm.js index 0667acded4..73f3fad47b 100644 --- a/src/http/api/routes/swarm.js +++ b/src/http/api/routes/swarm.js @@ -1,62 +1,47 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ +module.exports = [ + { method: '*', path: '/api/v0/swarm/peers', - config: { - handler: resources.swarm.peers.handler - } - }) - - api.route({ + handler: resources.swarm.peers.handler + }, + { method: '*', path: '/api/v0/swarm/addrs', - config: { - handler: resources.swarm.addrs.handler - } - }) - - api.route({ + handler: resources.swarm.addrs.handler + }, + { method: '*', path: '/api/v0/swarm/addrs/local', - config: { - handler: resources.swarm.localAddrs.handler - } - }) - - api.route({ + handler: resources.swarm.localAddrs.handler + }, + { method: '*', path: '/api/v0/swarm/connect', - config: { + options: { pre: [ { method: resources.swarm.connect.parseArgs, assign: 'args' } - ], - handler: resources.swarm.connect.handler - } - }) - - api.route({ + ] + }, + handler: resources.swarm.connect.handler + }, + { method: '*', path: '/api/v0/swarm/disconnect', - config: { + options: { pre: [ { method: resources.swarm.disconnect.parseArgs, assign: 'args' } - ], - handler: resources.swarm.disconnect.handler - } - }) - + ] + }, + handler: resources.swarm.disconnect.handler + } // TODO - // api.route({ + // { // method: '*', // path: '/api/v0/swarm/filters', - // config: { - // handler: resources.swarm.disconnect - // } - // }) -} + // handler: resources.swarm.disconnect + // } +] diff --git a/src/http/api/routes/version.js b/src/http/api/routes/version.js index 80287a4f3d..8333d40a80 100644 --- a/src/http/api/routes/version.js +++ b/src/http/api/routes/version.js @@ -1,13 +1,9 @@ 'use strict' -const resources = require('./../resources') +const resources = require('../resources') -module.exports = (server) => { - const api = server.select('API') - - api.route({ - method: '*', - path: '/api/v0/version', - handler: resources.version.get - }) +module.exports = { + method: '*', + path: '/api/v0/version', + handler: resources.version } diff --git a/src/http/api/routes/webui.js b/src/http/api/routes/webui.js index bec3007c03..3a0f1a1db0 100644 --- a/src/http/api/routes/webui.js +++ b/src/http/api/routes/webui.js @@ -2,26 +2,22 @@ const resources = require('../../gateway/resources') -module.exports = (server) => { - const gateway = server.select('API') - - gateway.route([ - { - method: '*', - path: '/ipfs/{cid*}', - config: { - pre: [ - { method: resources.gateway.checkCID, assign: 'args' } - ], - handler: resources.gateway.handler - } +module.exports = [ + { + method: '*', + path: '/ipfs/{cid*}', + options: { + pre: [ + { method: resources.gateway.checkCID, assign: 'args' } + ] }, - { - method: '*', - path: '/webui', - handler: (request, reply) => { - return reply().redirect().location('/ipfs/QmXc9raDM1M5G5fpBnVyQ71vR4gbnskwnB9iMEzBuLgvoZ') - } + handler: resources.gateway.handler + }, + { + method: '*', + path: '/webui', + handler (request, h) { + return h.redirect('/ipfs/QmXc9raDM1M5G5fpBnVyQ71vR4gbnskwnB9iMEzBuLgvoZ') } - ]) -} + } +] diff --git a/src/http/error-handler.js b/src/http/error-handler.js index 0c490d4639..e1027211f1 100644 --- a/src/http/error-handler.js +++ b/src/http/error-handler.js @@ -1,61 +1,48 @@ 'use strict' -const Hoek = require('hoek') - -module.exports = (api, server) => { - server.ext('onRequest', (request, reply) => { - request.handleError = handleError - reply.continue() - }) - - server.ext('onPreResponse', (request, reply) => { +module.exports = server => { + server.ext('onPreResponse', (request, h) => { const res = request.response - const req = request.raw.req - let statusCode = 200 - let msg = 'Sorry, something went wrong, please retrace your steps.' - let code = 1 - - if (res.isBoom) { - statusCode = res.output.payload.statusCode - msg = res.output.payload.message - - if (res.data && res.data.code !== undefined) { - code = res.data.code - } + if (!res.isBoom) { + return h.continue + } - if (res.message && res.isDeveloperError) { - msg = res.message.replace('Uncaught error: ', '') + const message = res.message || res.output.payload.message + const { statusCode } = res.output.payload + let code + + if (res.data && res.data.code != null) { + code = res.data.code + } else { + // Map status code to error code as defined by go-ipfs + // https://github.com/ipfs/go-ipfs-cmdkit/blob/0262a120012063c359727423ec703b9649eec447/error.go#L12-L20 + if (statusCode >= 400 && statusCode < 500) { + code = statusCode === 404 ? 3 : 1 + } else { + code = 0 } + } + if (process.env.DEBUG || statusCode >= 500) { + const { req } = request.raw const debug = { method: req.method, url: request.url.path, - headers: request.raw.req.headers, + headers: req.headers, info: request.info, payload: request.payload, response: res.output.payload } - api.log.error(res.stack) - server.log('error', debug) - - reply({ - Message: msg, - Code: code, - Type: 'error' - }).code(statusCode) - return + server.logger().error(debug) + server.logger().error(res) } - reply.continue() + return h.response({ + Message: message, + Code: code, + Type: 'error' + }).code(statusCode) }) } - -function handleError (error, errorMessage) { - if (errorMessage) { - return Hoek.assert(!error, errorMessage) - } - - return Hoek.assert(!error, error) -} diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index 4f6cc67b8c..77cb25fcb8 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -4,14 +4,16 @@ const debug = require('debug') const log = debug('jsipfs:http-gateway') log.error = debug('jsipfs:http-gateway:error') const pull = require('pull-stream') -const toPull = require('stream-to-pull-stream') +const pushable = require('pull-pushable') +const toStream = require('pull-stream-to-stream') const fileType = require('file-type') const mime = require('mime-types') -const Stream = require('readable-stream') -const CID = require('cids') +const { PassThrough } = require('readable-stream') +const Boom = require('boom') const { resolver } = require('ipfs-http-response') const PathUtils = require('../utils/path') +const { cidToString } = require('../../../utils/cid') function detectContentType (ref, chunk) { let fileSignature @@ -29,151 +31,127 @@ function detectContentType (ref, chunk) { } module.exports = { - checkCID: (request, reply) => { + checkCID (request, h) { if (!request.params.cid) { - return reply({ - Message: 'Path Resolve error: path must contain at least one component', - Code: 0, - Type: 'error' - }).code(400).takeover() + throw Boom.badRequest('Path Resolve error: path must contain at least one component') } - return reply({ - ref: `/ipfs/${request.params.cid}` - }) + return { ref: `/ipfs/${request.params.cid}` } }, - handler: (request, reply) => { - const ref = request.pre.args.ref - const ipfs = request.server.app.ipfs - - function handleGatewayResolverError (err) { - if (err) { - log.error('err: ', err.toString(), ' fileName: ', err.fileName) - - const errorToString = err.toString() - // switch case with true feels so wrong. - switch (true) { - case (errorToString === 'Error: This dag node is a directory'): - resolver.directory(ipfs, ref, err.fileName, (err, data) => { - if (err) { - log.error(err) - return reply(err.toString()).code(500) - } - if (typeof data === 'string') { - // no index file found - if (!ref.endsWith('/')) { - // for a directory, if URL doesn't end with a / - // append / and redirect permanent to that URL - return reply.redirect(`${ref}/`).permanent(true) - } else { - // send directory listing - return reply(data) - } - } else { - // found index file - // redirect to URL/ - return reply.redirect(PathUtils.joinURLParts(ref, data[0].name)) - } - }) - break - case (errorToString.startsWith('Error: no link named')): - return reply(errorToString).code(404) - case (errorToString.startsWith('Error: multihash length inconsistent')): - case (errorToString.startsWith('Error: Non-base58 character')): - return reply({ Message: errorToString, Code: 0, Type: 'error' }).code(400) - default: - log.error(err) - return reply({ Message: errorToString, Code: 0, Type: 'error' }).code(500) - } + async handler (request, h) { + const { ref } = request.pre.args + const { ipfs } = request.server.app + + let data + try { + data = await resolver.cid(ipfs, ref) + } catch (err) { + const errorToString = err.toString() + log.error('err: ', errorToString, ' fileName: ', err.fileName) + + // switch case with true feels so wrong. + switch (true) { + case (errorToString === 'Error: This dag node is a directory'): + data = await resolver.directory(ipfs, ref, err.cid) + + if (typeof data === 'string') { + // no index file found + if (!ref.endsWith('/')) { + // for a directory, if URL doesn't end with a / + // append / and redirect permanent to that URL + return h.redirect(`${ref}/`).permanent(true) + } + // send directory listing + return h.response(data) + } + + // found index file + // redirect to URL/ + return h.redirect(PathUtils.joinURLParts(ref, data[0].name)) + case (errorToString.startsWith('Error: no link named')): + throw Boom.boomify(err, { statusCode: 404 }) + case (errorToString.startsWith('Error: multihash length inconsistent')): + case (errorToString.startsWith('Error: Non-base58 character')): + throw Boom.boomify(err, { statusCode: 400 }) + default: + log.error(err) + throw err } } - return resolver.multihash(ipfs, ref, (err, data) => { - if (err) { - return handleGatewayResolverError(err) - } + if (ref.endsWith('/')) { + // remove trailing slash for files + return h.redirect(PathUtils.removeTrailingSlash(ref)).permanent(true) + } - const stream = ipfs.catReadableStream(data.multihash) - stream.once('error', (err) => { - if (err) { - log.error(err) - return reply(err.toString()).code(500) - } - }) - - if (ref.endsWith('/')) { - // remove trailing slash for files - return reply - .redirect(PathUtils.removeTrailingSlash(ref)) - .permanent(true) - } else { - if (!stream._read) { - stream._read = () => {} - stream._readableState = {} - } - - // response.continue() - let contentTypeDetected = false - let stream2 = new Stream.PassThrough({ highWaterMark: 1 }) - stream2.on('error', (err) => { - log.error('stream2 err: ', err) - }) - - let response = reply(stream2).hold() - - // Etag maps directly to an identifier for a specific version of a resource - // TODO: change to .cid.toBaseEncodedString() after switch to new js-ipfs-http-response - response.header('Etag', `"${data.multihash}"`) - - // Set headers specific to the immutable namespace - if (ref.startsWith('/ipfs/')) { - response.header('Cache-Control', 'public, max-age=29030400, immutable') - } - - pull( - toPull.source(stream), - pull.through((chunk) => { - // Guess content-type (only once) - if (chunk.length > 0 && !contentTypeDetected) { - let contentType = detectContentType(ref, chunk) - contentTypeDetected = true + return new Promise((resolve, reject) => { + let pusher + let started = false + + pull( + ipfs.catPullStream(data.cid), + pull.drain( + chunk => { + if (!started) { + started = true + pusher = pushable() + const res = h.response(toStream.source(pusher).pipe(new PassThrough())) + + // Etag maps directly to an identifier for a specific version of a resource + res.header('Etag', `"${data.cid}"`) + + // Set headers specific to the immutable namespace + if (ref.startsWith('/ipfs/')) { + res.header('Cache-Control', 'public, max-age=29030400, immutable') + } + + const contentType = detectContentType(ref, chunk) log('ref ', ref) log('mime-type ', contentType) if (contentType) { log('writing content-type header') - response.header('Content-Type', contentType) + res.header('Content-Type', contentType) } - response.send() + resolve(res) } + pusher.push(chunk) + }, + err => { + if (err) { + log.error(err) + + // We already started flowing, abort the stream + if (started) { + return pusher.end(err) + } - stream2.write(chunk) - }), - pull.onEnd(() => { - log('stream ended.') - stream2.end() - }) + return reject(err) + } + + pusher.end() + } ) - } + ) }) }, - afterHandler: (request, reply) => { - const response = request.response + afterHandler (request, h) { + const { response } = request if (response.statusCode === 200) { - const ref = request.pre.args.ref + const { ref } = request.pre.args response.header('X-Ipfs-Path', ref) if (ref.startsWith('/ipfs/')) { const rootCid = ref.split('/')[2] - const ipfsOrigin = new CID(rootCid).toV1().toBaseEncodedString('base32') + const ipfsOrigin = cidToString(rootCid, { base: 'base32' }) response.header('Suborigin', 'ipfs000' + ipfsOrigin) } // TODO: we don't have case-insensitive solution for /ipns/ yet (https://github.com/ipfs/go-ipfs/issues/5287) } - reply.continue() + return h.continue } } diff --git a/src/http/gateway/routes/gateway.js b/src/http/gateway/routes/gateway.js index 810a520437..eb8543d465 100644 --- a/src/http/gateway/routes/gateway.js +++ b/src/http/gateway/routes/gateway.js @@ -2,20 +2,16 @@ const resources = require('../resources') -module.exports = (server) => { - const gateway = server.select('Gateway') - - gateway.route({ - method: '*', - path: '/ipfs/{cid*}', - config: { - pre: [ - { method: resources.gateway.checkCID, assign: 'args' } - ], - handler: resources.gateway.handler, - ext: { - onPostHandler: { method: resources.gateway.afterHandler } - } +module.exports = { + method: '*', + path: '/ipfs/{cid*}', + options: { + handler: resources.gateway.handler, + pre: [ + { method: resources.gateway.checkCID, assign: 'args' } + ], + ext: { + onPostHandler: { method: resources.gateway.afterHandler } } - }) + } } diff --git a/src/http/gateway/routes/index.js b/src/http/gateway/routes/index.js index 0e0656c258..2cbf163b04 100644 --- a/src/http/gateway/routes/index.js +++ b/src/http/gateway/routes/index.js @@ -1,5 +1,3 @@ 'use strict' -module.exports = (server) => { - require('./gateway')(server) -} +module.exports = [require('./gateway')] diff --git a/src/http/index.js b/src/http/index.js index f35b05d075..ab5811835f 100644 --- a/src/http/index.js +++ b/src/http/index.js @@ -1,11 +1,10 @@ 'use strict' -const series = require('async/series') const Hapi = require('hapi') +const Pino = require('hapi-pino') const debug = require('debug') const multiaddr = require('multiaddr') -const setHeader = require('hapi-set-header') -const once = require('once') +const promisify = require('promisify-es6') const IPFS = require('../core') const WStar = require('libp2p-webrtc-star') @@ -20,177 +19,166 @@ function uriToMultiaddr (uri) { return `/ip4/${ipPort[0]}/tcp/${ipPort[1]}` } -function HttpApi (repo, config, cliArgs) { - cliArgs = cliArgs || {} - this.node = undefined - this.server = undefined - - this.log = debug('jsipfs:http-api') - this.log.error = debug('jsipfs:http-api:error') - - if (process.env.IPFS_MONITORING) { - // Setup debug metrics collection - const prometheusClient = require('prom-client') - const prometheusGcStats = require('prometheus-gc-stats') - const collectDefaultMetrics = prometheusClient.collectDefaultMetrics - collectDefaultMetrics({ timeout: 5000 }) - prometheusGcStats(prometheusClient.register)() +class HttpApi { + constructor (options) { + this._options = options || {} + this._log = debug('jsipfs:http-api') + this._log.error = debug('jsipfs:http-api:error') + + if (process.env.IPFS_MONITORING) { + // Setup debug metrics collection + const prometheusClient = require('prom-client') + const prometheusGcStats = require('prometheus-gc-stats') + const collectDefaultMetrics = prometheusClient.collectDefaultMetrics + collectDefaultMetrics({ timeout: 5000 }) + prometheusGcStats(prometheusClient.register)() + } } - this.start = (init, callback) => { - if (typeof init === 'function') { - callback = init - init = false + async start () { + this._log('starting') + + const libp2p = { modules: {} } + + // Attempt to use any of the WebRTC versions available globally + let electronWebRTC + let wrtc + try { + electronWebRTC = require('electron-webrtc')() + } catch (err) { + this._log('failed to load optional electron-webrtc dependency') + } + try { + wrtc = require('wrtc') + } catch (err) { + this._log('failed to load optional webrtc dependency') } - this.log('starting') - series([ - (cb) => { - cb = once(cb) + if (wrtc || electronWebRTC) { + const using = wrtc ? 'wrtc' : 'electron-webrtc' + this._log(`Using ${using} for webrtc support`) + const wstar = new WStar({ wrtc: (wrtc || electronWebRTC) }) + libp2p.modules.transport = [TCP, WS, wstar] + libp2p.modules.peerDiscovery = [MulticastDNS, Bootstrap, wstar.discovery] + } - const libp2p = { modules: {} } + // start the daemon + const ipfsOpts = Object.assign({ init: false }, this._options, { start: true, libp2p }) + const ipfs = new IPFS(ipfsOpts) + + await new Promise((resolve, reject) => { + ipfs.once('error', err => { + this._log('error starting core', err) + err.code = 'ENOENT' + reject(err) + }) + ipfs.once('start', resolve) + }) - // Attempt to use any of the WebRTC versions available globally - let electronWebRTC - let wrtc - try { - electronWebRTC = require('electron-webrtc')() - } catch (err) { - this.log('failed to load optional electron-webrtc dependency') - } - try { - wrtc = require('wrtc') - } catch (err) { - this.log('failed to load optional webrtc dependency') - } + this._ipfs = ipfs - if (wrtc || electronWebRTC) { - const using = wrtc ? 'wrtc' : 'electron-webrtc' - this.log(`Using ${using} for webrtc support`) - const wstar = new WStar({ wrtc: (wrtc || electronWebRTC) }) - libp2p.modules.transport = [TCP, WS, wstar] - libp2p.modules.peerDiscovery = [MulticastDNS, Bootstrap, wstar.discovery] - } + const config = await ipfs.config.get() - // try-catch so that programmer errors are not swallowed during testing - try { - // start the daemon - this.node = new IPFS({ - silent: cliArgs.silent, - repo: repo, - init: init, - start: true, - config: config, - offline: cliArgs.offline, - pass: cliArgs.pass, - EXPERIMENTAL: { - pubsub: cliArgs.enablePubsubExperiment, - ipnsPubsub: cliArgs.enableNamesysPubsub, - dht: cliArgs.enableDhtExperiment, - sharding: cliArgs.enableShardingExperiment - }, - libp2p: libp2p - }) - } catch (err) { - return cb(err) - } + const apiAddr = config.Addresses.API.split('/') + const apiServer = await this._createApiServer(apiAddr[2], apiAddr[4], ipfs) + await apiServer.start() + apiServer.info.ma = uriToMultiaddr(apiServer.info.uri) + this._apiServer = apiServer + + // for the CLI to know the where abouts of the API + await promisify(ipfs._repo.apiAddr.set)(apiServer.info.ma) + + const gatewayAddr = config.Addresses.Gateway.split('/') + const gatewayServer = await this._createGatewayServer(gatewayAddr[2], gatewayAddr[4], ipfs) + await gatewayServer.start() + gatewayServer.info.ma = uriToMultiaddr(gatewayServer.info.uri) + this._gatewayServer = gatewayServer - this.node.once('error', (err) => { - this.log('error starting core', err) - err.code = 'ENOENT' - cb(err) - }) - this.node.once('start', cb) - }, - (cb) => { - this.log('fetching config') - this.node._repo.config.get((err, config) => { - if (err) { - return callback(err) - } - - // CORS is enabled by default - // TODO: shouldn't, fix this - this.server = new Hapi.Server({ - connections: { - routes: { - cors: true - } - }, - debug: process.env.DEBUG ? { - request: ['*'], - log: ['*'] - } : undefined - }) - - this.server.app.ipfs = this.node - const api = config.Addresses.API.split('/') - const gateway = config.Addresses.Gateway.split('/') - - // select which connection with server.select(