From d948b50cd313101b95df3e18046cbb0f18891225 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 2 May 2018 15:46:04 +0100 Subject: [PATCH 1/3] feat: add IPLD to dag.get --- package.json | 2 + src/dag/get.js | 46 ++++++++------------- src/utils/ipfs-path.js | 74 +++++++++++++++++++++++++++++++++ test/utils-ipfs-path.spec.js | 79 ++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 28 deletions(-) create mode 100644 src/utils/ipfs-path.js create mode 100644 test/utils-ipfs-path.spec.js diff --git a/package.json b/package.json index 9e6b2899d..cbc1259d7 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,12 @@ "cids": "~0.5.3", "concat-stream": "^1.6.2", "detect-node": "^2.0.3", + "explain-error": "^1.0.4", "flatmap": "0.0.3", "glob": "^7.1.2", "ipfs-block": "~0.7.1", "ipfs-unixfs": "~0.1.14", + "ipld": "^0.17.0", "ipld-dag-cbor": "~0.12.0", "ipld-dag-pb": "~0.14.4", "is-ipfs": "~0.3.2", diff --git a/src/dag/get.js b/src/dag/get.js index ead6a732c..c9ca1cf0a 100644 --- a/src/dag/get.js +++ b/src/dag/get.js @@ -1,10 +1,9 @@ 'use strict' -const dagPB = require('ipld-dag-pb') -const dagCBOR = require('ipld-dag-cbor') const promisify = require('promisify-es6') -const CID = require('cids') -const waterfall = require('async/waterfall') +const IPLDResolver = require('ipld') +const explain = require('explain-error') +const ipfsPath = require('../utils/ipfs-path') const block = require('../block') module.exports = (send) => { @@ -22,31 +21,22 @@ module.exports = (send) => { options = options || {} path = path || '' - if (CID.isCID(cid)) { - cid = cid.toBaseEncodedString() + try { + const res = ipfsPath(cid) + cid = res.cid + path = res.path || path + } catch (err) { + return callback(err) } - waterfall([ - cb => { - send({ - path: 'dag/resolve', - args: cid + '/' + path, - qs: options - }, cb) - }, - (resolved, cb) => { - block(send).get(new CID(resolved['Cid']['/']), (err, ipfsBlock) => { - cb(err, ipfsBlock, resolved['RemPath']) - }) - }, - (ipfsBlock, path, cb) => { - if (ipfsBlock.cid.codec === 'dag-cbor') { - dagCBOR.resolver.resolve(ipfsBlock.data, path, cb) - } - if (ipfsBlock.cid.codec === 'dag-pb') { - dagPB.resolver.resolve(ipfsBlock.data, path, cb) - } - } - ], callback) + IPLDResolver.inMemory((err, ipld) => { + if (err) return callback(explain(err, 'failed to create IPLD resolver')) + + ipld.bs.setExchange({ + get: (cid, cb) => block(send).get(cid, cb) + }) + + ipld.get(cid, path, options, callback) + }) }) } diff --git a/src/utils/ipfs-path.js b/src/utils/ipfs-path.js new file mode 100644 index 000000000..a7ad518de --- /dev/null +++ b/src/utils/ipfs-path.js @@ -0,0 +1,74 @@ +'use strict' + +const CID = require('cids') +const explain = require('explain-error') + +// Parse an `input` as an IPFS path and return an object of it's component parts. +// +// `input` can be: +// +// `String` +// * an IPFS path like `/ipfs/Qmf1JJkBEk7nSdYZJqumJVREE1bMZS7uMm6DQFxRxWShwD/file.txt` +// * an IPNS path like `/ipns/yourdomain.name/file.txt` +// * a CID like `Qmf1JJkBEk7nSdYZJqumJVREE1bMZS7uMm6DQFxRxWShwD` +// * a CID and path like `Qmf1JJkBEk7nSdYZJqumJVREE1bMZS7uMm6DQFxRxWShwD/file.txt` +// `CID` - a CID instance +// `Buffer` - a Buffer CID +// +// The return value is an object with the following properties: +// +// * `cid: CID` - the content identifier +// * `path: String` - the path component of the dweb path (the bit after the cid) +module.exports = (input) => { + let cid, path + + if (Buffer.isBuffer(input) || CID.isCID(input)) { + try { + cid = new CID(input) + } catch (err) { + throw explain(err, 'invalid CID') + } + + path = '' + } else if (Object.prototype.toString.call(input) === '[object String]') { + // Ensure leading slash + if (input[0] !== '/') { + input = `/${input}` + } + + // Remove trailing slash + if (input[input.length - 1] === '/') { + input = input.slice(0, -1) + } + + const parts = input.split('/') + + if (parts[1] === 'ipfs') { + try { + cid = new CID(parts[2]) + } catch (err) { + throw explain(err, `invalid CID: ${parts[2]}`) + } + + path = parts.slice(3).join('/') + } else { + // Is parts[1] a CID? + try { + cid = new CID(parts[1]) + } catch (err) { + throw new Error(`unknown namespace: ${parts[1]}`) + } + + path = parts.slice(2).join('/') + } + + // Ensure leading slash on non empty path + if (path.length) { + path = `/${path}` + } + } else { + throw new Error('invalid path') // What even is this? + } + + return { cid, path } +} diff --git a/test/utils-ipfs-path.spec.js b/test/utils-ipfs-path.spec.js new file mode 100644 index 000000000..ea0a5b0ab --- /dev/null +++ b/test/utils-ipfs-path.spec.js @@ -0,0 +1,79 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const CID = require('cids') +const ipfsPath = require('../src/utils/ipfs-path') + +describe('utils/ipfs-path', () => { + it('should parse input as string CID', () => { + const input = 'QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn' + const { cid, path } = ipfsPath(input) + + expect(cid.toBaseEncodedString()).to.equal('QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn') + expect(path).to.equal('') + }) + + it('should parse input as buffer CID', () => { + const input = Buffer.from('017012207252523e6591fb8fe553d67ff55a86f84044b46a3e4176e10c58fa529a4aabd5', 'hex') + const { cid, path } = ipfsPath(input) + + expect(cid.toBaseEncodedString()).to.equal('zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS') + expect(path).to.equal('') + }) + + it('should parse input as CID instance', () => { + const input = new CID('zdpuArHMUAYi3VtD3f7iSkXxYK9xo687SoNf5stAQNCMzd77k') + const { cid, path } = ipfsPath(input) + + expect(cid.equals(input)).to.equal(true) + expect(path).to.equal('') + }) + + it('should parse input as string with path and without namespace', () => { + const input = 'QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn/path/to' + const { cid, path } = ipfsPath(input) + + expect(cid.toBaseEncodedString()).to.equal('QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn') + expect(path).to.equal('/path/to') + }) + + it('should parse input as string without leading slash', () => { + const input = 'ipfs/QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn/path/to' + const { cid, path } = ipfsPath(input) + + expect(cid.toBaseEncodedString()).to.equal('QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn') + expect(path).to.equal('/path/to') + }) + + it('should parse input as string with trailing slash', () => { + const input = '/ipfs/QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn/path/to/' + const { cid, path } = ipfsPath(input) + + expect(cid.toBaseEncodedString()).to.equal('QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn') + expect(path).to.equal('/path/to') + }) + + it('should throw on unknown namespace', () => { + const input = '/junk/stuff' + expect(() => ipfsPath(input)).to.throw('unknown namespace: junk') + }) + + it('should throw on invalid CID in string', () => { + const input = '/ipfs/notACID/some/path' + expect(() => ipfsPath(input)).to.throw('invalid CID') + }) + + it('should throw on invalid CID in buffer', () => { + const input = Buffer.from('notaCID') + expect(() => ipfsPath(input)).to.throw('invalid CID') + }) + + it('should throw on invalid path', () => { + const input = 42 + expect(() => ipfsPath(input)).to.throw('invalid path') + }) +}) From 2ec107e30ad55bd3d09854eb72a730c0ac6a4269 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 2 May 2018 15:50:33 +0100 Subject: [PATCH 2/3] extract block get --- src/dag/get.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/dag/get.js b/src/dag/get.js index c9ca1cf0a..27733bcda 100644 --- a/src/dag/get.js +++ b/src/dag/get.js @@ -7,6 +7,8 @@ const ipfsPath = require('../utils/ipfs-path') const block = require('../block') module.exports = (send) => { + const blockGet = block(send).get + return promisify((cid, path, options, callback) => { if (typeof path === 'function') { callback = path @@ -31,11 +33,7 @@ module.exports = (send) => { IPLDResolver.inMemory((err, ipld) => { if (err) return callback(explain(err, 'failed to create IPLD resolver')) - - ipld.bs.setExchange({ - get: (cid, cb) => block(send).get(cid, cb) - }) - + ipld.bs.setExchange({ get: blockGet }) ipld.get(cid, path, options, callback) }) }) From eba2ad0c231ae3bdc5250e87914dd54b59fab71a Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 9 May 2018 15:40:29 +0100 Subject: [PATCH 3/3] feat(dag): use IPFS resolver License: MIT Signed-off-by: Alan Shaw --- src/dag/get.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dag/get.js b/src/dag/get.js index 27733bcda..ca397a475 100644 --- a/src/dag/get.js +++ b/src/dag/get.js @@ -3,6 +3,7 @@ const promisify = require('promisify-es6') const IPLDResolver = require('ipld') const explain = require('explain-error') +const dagPB = require('ipld-dag-pb/src/ipfs') const ipfsPath = require('../utils/ipfs-path') const block = require('../block') @@ -33,6 +34,8 @@ module.exports = (send) => { IPLDResolver.inMemory((err, ipld) => { if (err) return callback(explain(err, 'failed to create IPLD resolver')) + ipld.support.rm(dagPB.resolver.multicodec) + ipld.support.add(dagPB.resolver.multicodec, dagPB.resolver, dagPB.util) ipld.bs.setExchange({ get: blockGet }) ipld.get(cid, path, options, callback) })