Skip to content
This repository has been archived by the owner on Mar 10, 2020. It is now read-only.

[WIP] feat(dag): add IPLD to dag.get #755

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 19 additions & 28 deletions src/dag/get.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
'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 dagPB = require('ipld-dag-pb/src/ipfs')
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
Expand All @@ -22,31 +24,20 @@ 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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking that this should be done by passing the ipfs.block.{get,put} as the Block Service, rather than doing an inMemory resolver and then monkey patching the Block Service Instance with a fake exchange.

You should be able to do:

const ipld = new IPLD(ipfs.block)

or close

const ipld = new IPLD(shimBS(ipfs.block))

if (err) return callback(explain(err, 'failed to create IPLD resolver'))
ipld.support.rm(dagPB.resolver.multicodec)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a comment here, why this is needed? It's clear to me, but I don't think it's clear when you read the code for the first time.

ipld.support.add(dagPB.resolver.multicodec, dagPB.resolver, dagPB.util)
ipld.bs.setExchange({ get: blockGet })
ipld.get(cid, path, options, callback)
})
})
}
74 changes: 74 additions & 0 deletions src/utils/ipfs-path.js
Original file line number Diff line number Diff line change
@@ -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 }
}
79 changes: 79 additions & 0 deletions test/utils-ipfs-path.spec.js
Original file line number Diff line number Diff line change
@@ -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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this actually be supported? I prefer having strict rules and fail early. The rule would be: path with namespaces start with a slash, if there isn't a slash it's a CID.

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')
})
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional test cases:

  • what happens if a path has two slashes in a row (/ipfs/CID/some//thing)
  • ipfs namespace without a path (/ipfs/CID)
  • CID-only with a trailing slash, with and without namespace (CID/, /ipfs/CID/)