diff --git a/src/cli/commands/resolve.js b/src/cli/commands/resolve.js new file mode 100644 index 0000000000..c1b7a54991 --- /dev/null +++ b/src/cli/commands/resolve.js @@ -0,0 +1,24 @@ +'use strict' + +const print = require('../utils').print + +module.exports = { + command: 'resolve ', + + description: 'Resolve the value of names to IPFS', + + builder: { + recursive: { + alias: 'r', + type: 'boolean', + default: false + } + }, + + handler (argv) { + argv.ipfs.resolve(argv.name, { recursive: argv.recursive }, (err, res) => { + if (err) throw err + print(res) + }) + } +} diff --git a/src/core/components/index.js b/src/core/components/index.js index 1f6f084dee..6fc833499d 100644 --- a/src/core/components/index.js +++ b/src/core/components/index.js @@ -27,3 +27,4 @@ exports.dns = require('./dns') exports.key = require('./key') exports.stats = require('./stats') exports.mfs = require('./mfs') +exports.resolve = require('./resolve') diff --git a/src/core/components/resolve.js b/src/core/components/resolve.js new file mode 100644 index 0000000000..dd866cbfbb --- /dev/null +++ b/src/core/components/resolve.js @@ -0,0 +1,86 @@ +'use strict' + +const promisify = require('promisify-es6') +const isIpfs = require('is-ipfs') +const setImmediate = require('async/setImmediate') +const doUntil = require('async/doUntil') +const CID = require('cids') + +module.exports = (self) => { + return promisify((name, opts, cb) => { + if (typeof opts === 'function') { + cb = opts + opts = {} + } + + opts = opts || {} + + if (!isIpfs.path(name)) { + return setImmediate(() => cb(new Error('invalid argument'))) + } + + // TODO remove this and update subsequent code when IPNS is implemented + if (!isIpfs.ipfsPath(name)) { + return setImmediate(() => cb(new Error('resolve non-IPFS names is not implemented'))) + } + + const split = name.split('/') // ['', 'ipfs', 'hash', ...path] + const cid = new CID(split[2]) + + if (split.length === 3) { + return setImmediate(() => cb(null, name)) + } + + const path = split.slice(3).join('/') + + resolve(cid, path, (err, cid) => { + if (err) return cb(err) + if (!cid) return cb(new Error('found non-link at given path')) + cb(null, `/ipfs/${cid.toBaseEncodedString(opts.cidBase)}`) + }) + }) + + // Resolve the given CID + path to a CID. + function resolve (cid, path, callback) { + let value + + doUntil( + (cb) => { + self.block.get(cid, (err, block) => { + if (err) return cb(err) + + const r = self._ipld.resolvers[cid.codec] + + if (!r) { + return cb(new Error(`No resolver found for codec "${cid.codec}"`)) + } + + r.resolver.resolve(block.data, path, (err, result) => { + if (err) return cb(err) + value = result.value + path = result.remainderPath + cb() + }) + }) + }, + () => { + const endReached = !path || path === '/' + + if (endReached) { + return true + } + + if (value) { + cid = new CID(value['/']) + } + + return false + }, + (err) => { + if (err) return callback(err) + if (value && value['/']) return callback(null, new CID(value['/'])) + callback() + } + ) + } +} diff --git a/src/core/index.js b/src/core/index.js index 52266b7b41..f9b29c5145 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -120,6 +120,7 @@ class IPFS extends EventEmitter { this.dns = components.dns(this) this.key = components.key(this) this.stats = components.stats(this) + this.resolve = components.resolve(this) if (this._options.EXPERIMENTAL.pubsub) { this.log('EXPERIMENTAL pubsub is enabled') diff --git a/src/http/api/resources/index.js b/src/http/api/resources/index.js index 59040a99d8..f937bf2e1b 100644 --- a/src/http/api/resources/index.js +++ b/src/http/api/resources/index.js @@ -18,3 +18,4 @@ exports.pubsub = require('./pubsub') exports.dns = require('./dns') exports.key = require('./key') exports.stats = require('./stats') +exports.resolve = require('./resolve') diff --git a/src/http/api/resources/resolve.js b/src/http/api/resources/resolve.js new file mode 100644 index 0000000000..417f50d8e8 --- /dev/null +++ b/src/http/api/resources/resolve.js @@ -0,0 +1,37 @@ +'use strict' + +const Joi = require('joi') +const debug = require('debug') + +const log = debug('jsipfs:http-api:resolve') +log.error = debug('jsipfs:http-api:resolve:error') + +module.exports = { + validate: { + query: Joi.object().keys({ + r: Joi.alternatives() + .when('recursive', { + is: Joi.any().exist(), + then: Joi.any().forbidden(), + otherwise: Joi.boolean() + }), + recursive: Joi.boolean(), + arg: Joi.string().required() + }).unknown() + }, + handler (request, reply) { + const ipfs = request.server.app.ipfs + const name = request.query.arg + const recursive = request.query.r || request.query.recursive || false + + log(name, { recursive }) + + ipfs.resolve(name, { recursive }, (err, res) => { + if (err) { + log.error(err) + return reply({ Message: err.message, Code: 0 }).code(500) + } + reply({ Path: res }) + }) + } +} diff --git a/src/http/api/routes/index.js b/src/http/api/routes/index.js index bfec26a460..4087299ecd 100644 --- a/src/http/api/routes/index.js +++ b/src/http/api/routes/index.js @@ -21,4 +21,5 @@ module.exports = (server) => { require('./dns')(server) require('./key')(server) require('./stats')(server) + require('./resolve')(server) } diff --git a/src/http/api/routes/resolve.js b/src/http/api/routes/resolve.js new file mode 100644 index 0000000000..259ae3bdd1 --- /dev/null +++ b/src/http/api/routes/resolve.js @@ -0,0 +1,16 @@ +'use strict' + +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 + } + }) +} diff --git a/test/cli/commands.js b/test/cli/commands.js index 2c0c5fc033..2c52e23d74 100644 --- a/test/cli/commands.js +++ b/test/cli/commands.js @@ -4,7 +4,7 @@ const expect = require('chai').expect const runOnAndOff = require('../utils/on-and-off') -const commandCount = 77 +const commandCount = 78 describe('commands', () => runOnAndOff((thing) => { let ipfs diff --git a/test/cli/resolve.js b/test/cli/resolve.js new file mode 100644 index 0000000000..fe670d4d15 --- /dev/null +++ b/test/cli/resolve.js @@ -0,0 +1,56 @@ +/* eslint-env mocha */ +'use strict' + +const path = require('path') +const expect = require('chai').expect +const isIpfs = require('is-ipfs') + +const runOnAndOff = require('../utils/on-and-off') + +describe('resolve', () => runOnAndOff((thing) => { + let ipfs + + before(() => { + ipfs = thing.ipfs + }) + + it('should resolve an IPFS hash', function () { + this.timeout(10 * 1000) + + const filePath = path.join(process.cwd(), '/src/init-files/init-docs/readme') + let hash + + return ipfs(`add ${filePath}`) + .then((out) => { + hash = out.split(' ')[1] + expect(isIpfs.cid(hash)).to.be.true() + return ipfs(`resolve /ipfs/${hash}`) + }) + .then((out) => { + expect(out).to.contain(`/ipfs/${hash}`) + }) + }) + + it('should resolve an IPFS path link', function () { + this.timeout(10 * 1000) + + const filePath = path.join(process.cwd(), '/src/init-files/init-docs/readme') + let fileHash, rootHash + + return ipfs(`add ${filePath} --wrap-with-directory`) + .then((out) => { + const lines = out.split('\n') + + fileHash = lines[0].split(' ')[1] + rootHash = lines[1].split(' ')[1] + + expect(isIpfs.cid(fileHash)).to.be.true() + expect(isIpfs.cid(rootHash)).to.be.true() + + return ipfs(`resolve /ipfs/${rootHash}/readme`) + }) + .then((out) => { + expect(out).to.contain(`/ipfs/${fileHash}`) + }) + }) +})) diff --git a/test/core/interface.spec.js b/test/core/interface.spec.js index 44ccf730dd..8ac1655b97 100644 --- a/test/core/interface.spec.js +++ b/test/core/interface.spec.js @@ -55,8 +55,12 @@ describe('interface-ipfs-core tests', () => { }), { skip: [ { - name: 'resolve', - reason: 'TODO: not implemented' + name: 'should resolve an IPNS DNS link', + reason: 'TODO IPNS not implemented yet' + }, + { + name: 'should resolve IPNS link recursively', + reason: 'TODO IPNS not implemented yet' } ] }) diff --git a/test/http-api/interface.js b/test/http-api/interface.js index b27c2b22fb..ac33dfb370 100644 --- a/test/http-api/interface.js +++ b/test/http-api/interface.js @@ -40,8 +40,12 @@ describe('interface-ipfs-core over ipfs-api tests', () => { }), { skip: [ { - name: 'resolve', - reason: 'TODO: not implemented' + name: 'should resolve an IPNS DNS link', + reason: 'TODO IPNS not implemented yet' + }, + { + name: 'should resolve IPNS link recursively', + reason: 'TODO IPNS not implemented yet' } ] })