Skip to content

Commit 7f7f76a

Browse files
authored
feat: support loading arbitrary ipld formats in the http client (#3073)
Adds support for handling more than just `dag-pb`, `dag-cbor` and `raw` codecs in the client. Also adds docs on how to configure the IPFS node you are sending the data to as it needs to know how to handle the formats too.
1 parent 9f998e1 commit 7f7f76a

File tree

3 files changed

+126
-10
lines changed

3 files changed

+126
-10
lines changed

README.md

+50
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
- [URL source](#url-source)
5959
- [`urlSource(url)`](#urlsourceurl)
6060
- [Example](#example-1)
61+
- [IPLD Formats](#ipld-formats)
6162
- [Running the daemon with the right port](#running-the-daemon-with-the-right-port)
6263
- [Importing the module and usage](#importing-the-module-and-usage)
6364
- [Importing a sub-module and usage](#importing-a-sub-module-and-usage)
@@ -95,6 +96,8 @@ All core API methods take _additional_ `options` specific to the HTTP API:
9596

9697
* `headers` - An object or [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) instance that can be used to set custom HTTP headers. Note that this option can also be [configured globally](#custom-headers) via the constructor options.
9798
* `searchParams` - An object or [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) instance that can be used to add additional query parameters to the query string sent with each request.
99+
* `ipld.formats` - An array of additional [IPLD formats](https://github.com/ipld/interface-ipld-format) to support
100+
* `ipld.loadFormat` an async function that takes the name of an [IPLD format](https://github.com/ipld/interface-ipld-format) as a string and should return the implementation of that codec
98101

99102
### Instance Utils
100103

@@ -191,6 +194,53 @@ for await (const file of ipfs.add(urlSource('https://ipfs.io/images/ipfs-logo.sv
191194
*/
192195
```
193196

197+
### IPLD Formats
198+
199+
By default an instance of the client supports the following [IPLD formats](https://github.com/ipld/interface-ipld-format), which are enough to do all core IPFS operations:
200+
201+
* [dag-pb](https://github.com/ipld/specs/blob/master/block-layer/codecs/dag-pb.md)
202+
* [dag-cbor](https://github.com/ipld/specs/blob/master/block-layer/codecs/dag-cbor.md)
203+
* [raw](https://github.com/ipld/specs/issues/223)
204+
205+
If your application requires support for extra codecs, you can configure them as follows:
206+
207+
1. Configure the [IPLD layer](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#optionsipld) of your IPFS daemon to support the codec. This step is necessary so the node knows how to prepare data received over HTTP to be passed to IPLD for serialization:
208+
```javascript
209+
const ipfs = require('ipfs')
210+
211+
const node = await ipfs({
212+
ipld: {
213+
// either specify them as part of the `formats` list
214+
formats: [
215+
require('my-format')
216+
],
217+
218+
// or supply a function to load them dynamically
219+
loadFormat: async (format) => {
220+
return require(format)
221+
}
222+
}
223+
})
224+
2. Configure your IPFS HTTP API Client to support the codec. This is necessary so that the client can send the data to the IPFS node over HTTP:
225+
```javascript
226+
const ipfsHttpClient = require('ipfs-http-client')
227+
228+
const client = ipfsHttpClient({
229+
url: 'http://127.0.0.1:5002',
230+
ipld: {
231+
// either specify them as part of the `formats` list
232+
formats: [
233+
require('my-format')
234+
],
235+
236+
// or supply a function to load them dynamically
237+
loadFormat: async (format) => {
238+
return require(format)
239+
}
240+
}
241+
})
242+
```
243+
194244
### Running the daemon with the right port
195245

196246
To interact with the API, you need to have a local daemon running. It needs to be open on the right port. `5001` is the default, and is used in the examples below, but it can be set to whatever you need.

src/dag/put.js

+32-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
'use strict'
22

33
const dagCBOR = require('ipld-dag-cbor')
4+
const dagPB = require('ipld-dag-pb')
5+
const ipldRaw = require('ipld-raw')
46
const CID = require('cids')
57
const multihash = require('multihashes')
68
const configure = require('../lib/configure')
79
const multipartRequest = require('../lib/multipart-request')
810
const toUrlSearchParams = require('../lib/to-url-search-params')
911
const anySignal = require('any-signal')
1012
const AbortController = require('abort-controller')
13+
const multicodec = require('multicodec')
14+
15+
module.exports = configure((api, opts) => {
16+
const formats = {
17+
[multicodec.DAG_PB]: dagPB,
18+
[multicodec.DAG_CBOR]: dagCBOR,
19+
[multicodec.RAW]: ipldRaw
20+
}
21+
22+
const ipldOptions = (opts && opts.ipld) || {}
23+
const configuredFormats = (ipldOptions && ipldOptions.formats) || []
24+
configuredFormats.forEach(format => {
25+
formats[format.codec] = format
26+
})
1127

12-
module.exports = configure(api => {
1328
return async (dagNode, options = {}) => {
1429
if (options.cid && (options.format || options.hashAlg)) {
1530
throw new Error('Failed to put DAG node. Provide either `cid` OR `format` and `hashAlg` options')
@@ -34,17 +49,25 @@ module.exports = configure(api => {
3449
...options
3550
}
3651

37-
let serialized
52+
const number = multicodec.getNumber(options.format)
53+
let format = formats[number]
54+
55+
if (!format) {
56+
if (opts && opts.ipld && opts.ipld.loadFormat) {
57+
format = await opts.ipld.loadFormat(options.format)
58+
}
3859

39-
if (options.format === 'dag-cbor') {
40-
serialized = dagCBOR.util.serialize(dagNode)
41-
} else if (options.format === 'dag-pb') {
42-
serialized = dagNode.serialize()
43-
} else {
44-
// FIXME Hopefully already serialized...can we use IPLD to serialise instead?
45-
serialized = dagNode
60+
if (!format) {
61+
throw new Error('Format unsupported - please add support using the options.ipld.formats or options.ipld.loadFormat options')
62+
}
63+
}
64+
65+
if (!format.util || !format.util.serialize) {
66+
throw new Error('Format does not support utils.serialize function')
4667
}
4768

69+
const serialized = format.util.serialize(dagNode)
70+
4871
// allow aborting requests on body errors
4972
const controller = new AbortController()
5073
const signal = anySignal([controller.signal, options.signal])

test/dag.spec.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { expect } = require('interface-ipfs-core/src/utils/mocha')
88
const { DAGNode } = require('ipld-dag-pb')
99
const CID = require('cids')
1010
const f = require('./utils/factory')()
11+
const ipfsHttpClient = require('../src')
1112

1213
let ipfs
1314

@@ -27,7 +28,6 @@ describe('.dag', function () {
2728
cid = cid.toV0()
2829
expect(cid.codec).to.equal('dag-pb')
2930
cid = cid.toBaseEncodedString('base58btc')
30-
// expect(cid).to.equal('bafybeig3t3eugdchignsgkou3ly2mmy4ic4gtfor7inftnqn3yq4ws3a5u')
3131
expect(cid).to.equal('Qmd7xRhW5f29QuBFtqu3oSD27iVy35NRB91XFjmKFhtgMr')
3232

3333
const result = await ipfs.dag.get(cid)
@@ -48,11 +48,54 @@ describe('.dag', function () {
4848
expect(result.value).to.deep.equal(cbor)
4949
})
5050

51+
it('should be able to put and get a DAG node with format raw', async () => {
52+
const node = Buffer.from('some data')
53+
let cid = await ipfs.dag.put(node, { format: 'raw', hashAlg: 'sha2-256' })
54+
55+
expect(cid.codec).to.equal('raw')
56+
cid = cid.toBaseEncodedString('base32')
57+
expect(cid).to.equal('bafkreiata6mq425fzikf5m26temcvg7mizjrxrkn35swuybmpah2ajan5y')
58+
59+
const result = await ipfs.dag.get(cid)
60+
61+
expect(result.value).to.deep.equal(node)
62+
})
63+
5164
it('should error when missing DAG resolver for multicodec from requested CID', async () => {
5265
const block = await ipfs.block.put(Buffer.from([0, 1, 2, 3]), {
5366
cid: new CID('z8mWaJ1dZ9fH5EetPuRsj8jj26pXsgpsr')
5467
})
5568

5669
await expect(ipfs.dag.get(block.cid)).to.be.rejectedWith('Missing IPLD format "git-raw"')
5770
})
71+
72+
it('should error when putting node with esoteric format', () => {
73+
const node = Buffer.from('some data')
74+
75+
return expect(ipfs.dag.put(node, { format: 'git-raw', hashAlg: 'sha2-256' })).to.eventually.be.rejectedWith(/Format unsupported/)
76+
})
77+
78+
it('should attempt to load an unsupported format', async () => {
79+
let askedToLoadFormat
80+
const ipfs2 = ipfsHttpClient({
81+
url: `http://${ipfs.apiHost}:${ipfs.apiPort}`,
82+
ipld: {
83+
loadFormat: (format) => {
84+
askedToLoadFormat = format === 'git-raw'
85+
return {
86+
util: {
87+
serialize: (buf) => buf
88+
}
89+
}
90+
}
91+
}
92+
})
93+
94+
const node = Buffer.from('some data')
95+
96+
// error is from go-ipfs, this means the client serialized it ok
97+
await expect(ipfs2.dag.put(node, { format: 'git-raw', hashAlg: 'sha2-256' })).to.eventually.be.rejectedWith(/no parser for format "git-raw"/)
98+
99+
expect(askedToLoadFormat).to.be.true()
100+
})
58101
})

0 commit comments

Comments
 (0)