Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit a033e8b

Browse files
achingbrainAlan Shaw
authored and
Alan Shaw
committed
feat: add HTTP DAG API (#1930)
1 parent b841f1c commit a033e8b

File tree

6 files changed

+749
-1
lines changed

6 files changed

+749
-1
lines changed

src/http/api/resources/dag.js

+281
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
'use strict'
2+
3+
const promisify = require('promisify-es6')
4+
const CID = require('cids')
5+
const multipart = require('ipfs-multipart')
6+
const mh = require('multihashes')
7+
const Joi = require('joi')
8+
const multibase = require('multibase')
9+
const Boom = require('boom')
10+
const debug = require('debug')
11+
const {
12+
cidToString
13+
} = require('../../../utils/cid')
14+
const log = debug('ipfs:http-api:dag')
15+
log.error = debug('ipfs:http-api:dag:error')
16+
17+
// common pre request handler that parses the args and returns `key` which is assigned to `request.pre.args`
18+
exports.parseKey = (argument = 'Argument', name = 'key', quote = "'") => {
19+
return (request) => {
20+
if (!request.query.arg) {
21+
// for compatibility with go error messages
22+
throw Boom.badRequest(`${argument} ${quote}${name}${quote} is required`)
23+
}
24+
25+
let key = request.query.arg.trim()
26+
let path
27+
28+
if (key.startsWith('/ipfs')) {
29+
key = key.substring(5)
30+
}
31+
32+
const parts = key.split('/')
33+
34+
if (parts.length > 1) {
35+
key = parts.shift()
36+
path = `${parts.join('/')}`
37+
}
38+
39+
if (path && path.endsWith('/')) {
40+
path = path.substring(0, path.length - 1)
41+
}
42+
43+
try {
44+
return {
45+
[name]: new CID(key),
46+
path
47+
}
48+
} catch (err) {
49+
log.error(err)
50+
throw Boom.badRequest("invalid 'ipfs ref' path")
51+
}
52+
}
53+
}
54+
55+
const encodeBufferKeys = (obj, encoding) => {
56+
if (!obj) {
57+
return obj
58+
}
59+
60+
if (Buffer.isBuffer(obj)) {
61+
return obj.toString(encoding)
62+
}
63+
64+
Object.keys(obj).forEach(key => {
65+
if (Buffer.isBuffer(obj)) {
66+
obj[key] = obj[key].toString(encoding)
67+
68+
return
69+
}
70+
71+
if (typeof obj[key] === 'object') {
72+
obj[key] = encodeBufferKeys(obj[key], encoding)
73+
}
74+
})
75+
76+
return obj
77+
}
78+
79+
exports.get = {
80+
validate: {
81+
query: Joi.object().keys({
82+
'data-encoding': Joi.string().valid(['text', 'base64', 'hex']).default('text'),
83+
'cid-base': Joi.string().valid(multibase.names)
84+
}).unknown()
85+
},
86+
87+
// uses common parseKey method that returns a `key`
88+
parseArgs: exports.parseKey(),
89+
90+
// main route handler which is called after the above `parseArgs`, but only if the args were valid
91+
async handler (request, h) {
92+
const {
93+
key,
94+
path
95+
} = request.pre.args
96+
const { ipfs } = request.server.app
97+
98+
let dataEncoding = request.query['data-encoding']
99+
100+
if (dataEncoding === 'text') {
101+
dataEncoding = 'utf8'
102+
}
103+
104+
let result
105+
106+
try {
107+
result = await ipfs.dag.get(key, path)
108+
} catch (err) {
109+
throw Boom.badRequest(err)
110+
}
111+
112+
try {
113+
result.value = encodeBufferKeys(result.value, dataEncoding)
114+
} catch (err) {
115+
throw Boom.boomify(err)
116+
}
117+
118+
return h.response(result.value)
119+
}
120+
}
121+
122+
exports.put = {
123+
validate: {
124+
query: Joi.object().keys({
125+
format: Joi.string().default('cbor'),
126+
'input-enc': Joi.string().default('json'),
127+
pin: Joi.boolean(),
128+
hash: Joi.string().valid(Object.keys(mh.names)).default('sha2-256'),
129+
'cid-base': Joi.string().valid(multibase.names).default('base58btc')
130+
}).unknown()
131+
},
132+
133+
// pre request handler that parses the args and returns `node`
134+
// which is assigned to `request.pre.args`
135+
async parseArgs (request, h) {
136+
if (!request.payload) {
137+
throw Boom.badRequest("File argument 'object data' is required")
138+
}
139+
140+
const enc = request.query['input-enc']
141+
142+
if (!request.headers['content-type']) {
143+
throw Boom.badRequest("File argument 'object data' is required")
144+
}
145+
146+
const fileStream = await new Promise((resolve, reject) => {
147+
multipart.reqParser(request.payload)
148+
.on('file', (name, stream) => resolve(stream))
149+
.on('end', () => reject(Boom.badRequest("File argument 'object data' is required")))
150+
})
151+
152+
let data = await new Promise((resolve, reject) => {
153+
fileStream
154+
.on('data', data => resolve(data))
155+
.on('end', () => reject(Boom.badRequest("File argument 'object data' is required")))
156+
})
157+
158+
let format = request.query.format
159+
160+
if (format === 'cbor') {
161+
format = 'dag-cbor'
162+
}
163+
164+
let node
165+
166+
if (format === 'raw') {
167+
node = data
168+
} else if (enc === 'json') {
169+
try {
170+
node = JSON.parse(data.toString())
171+
} catch (err) {
172+
throw Boom.badRequest('Failed to parse the JSON: ' + err)
173+
}
174+
} else {
175+
const { ipfs } = request.server.app
176+
const codec = ipfs._ipld.resolvers[format]
177+
178+
if (!codec) {
179+
throw Boom.badRequest(`Missing IPLD format "${request.query.format}"`)
180+
}
181+
182+
const deserialize = promisify(codec.util.deserialize)
183+
184+
node = await deserialize(data)
185+
}
186+
187+
return {
188+
node,
189+
format,
190+
hashAlg: request.query.hash
191+
}
192+
},
193+
194+
// main route handler which is called after the above `parseArgs`, but only if the args were valid
195+
async handler (request, h) {
196+
const { ipfs } = request.server.app
197+
const { node, format, hashAlg } = request.pre.args
198+
199+
let cid
200+
201+
try {
202+
cid = await ipfs.dag.put(node, {
203+
format: format,
204+
hashAlg: hashAlg
205+
})
206+
} catch (err) {
207+
throw Boom.boomify(err, { message: 'Failed to put node' })
208+
}
209+
210+
if (request.query.pin) {
211+
await ipfs.pin.add(cid)
212+
}
213+
214+
return h.response({
215+
Cid: {
216+
'/': cidToString(cid, {
217+
base: request.query['cid-base']
218+
})
219+
}
220+
})
221+
}
222+
}
223+
224+
exports.resolve = {
225+
validate: {
226+
query: Joi.object().keys({
227+
'cid-base': Joi.string().valid(multibase.names)
228+
}).unknown()
229+
},
230+
231+
// uses common parseKey method that returns a `key`
232+
parseArgs: exports.parseKey('argument', 'ref', '"'),
233+
234+
// main route handler which is called after the above `parseArgs`, but only if the args were valid
235+
async handler (request, h) {
236+
let { ref, path } = request.pre.args
237+
const { ipfs } = request.server.app
238+
239+
// to be consistent with go we need to return the CID to the last node we've traversed
240+
// along with the path inside that node as the remainder path
241+
try {
242+
let lastCid = ref
243+
let lastRemainderPath = path
244+
245+
while (true) {
246+
const block = await ipfs.block.get(lastCid)
247+
const codec = ipfs._ipld.resolvers[lastCid.codec]
248+
249+
if (!codec) {
250+
throw Boom.badRequest(`Missing IPLD format "${lastCid.codec}"`)
251+
}
252+
253+
const resolve = promisify(codec.resolver.resolve)
254+
const res = await resolve(block.data, lastRemainderPath)
255+
256+
if (!res.remainderPath) {
257+
break
258+
}
259+
260+
lastRemainderPath = res.remainderPath
261+
262+
if (!CID.isCID(res.value)) {
263+
break
264+
}
265+
266+
lastCid = res.value
267+
}
268+
269+
return h.response({
270+
Cid: {
271+
'/': cidToString(lastCid, {
272+
base: request.query['cid-base']
273+
})
274+
},
275+
RemPath: lastRemainderPath || ''
276+
})
277+
} catch (err) {
278+
throw Boom.boomify(err)
279+
}
280+
}
281+
}

src/http/api/resources/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ exports.bitswap = require('./bitswap')
1515
exports.file = require('./file')
1616
exports.filesRegular = require('./files-regular')
1717
exports.pubsub = require('./pubsub')
18+
exports.dag = require('./dag')
1819
exports.dns = require('./dns')
1920
exports.key = require('./key')
2021
exports.stats = require('./stats')

src/http/api/routes/dag.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
const resources = require('../resources')
4+
5+
module.exports = [
6+
{
7+
method: 'POST',
8+
path: '/api/v0/dag/get',
9+
options: {
10+
pre: [
11+
{ method: resources.dag.get.parseArgs, assign: 'args' }
12+
],
13+
validate: resources.dag.get.validate
14+
},
15+
handler: resources.dag.get.handler
16+
},
17+
{
18+
method: 'POST',
19+
path: '/api/v0/dag/put',
20+
options: {
21+
payload: {
22+
parse: false,
23+
output: 'stream'
24+
},
25+
pre: [
26+
{ method: resources.dag.put.parseArgs, assign: 'args' }
27+
],
28+
validate: resources.dag.put.validate
29+
},
30+
handler: resources.dag.put.handler
31+
},
32+
{
33+
method: 'POST',
34+
path: '/api/v0/dag/resolve',
35+
options: {
36+
pre: [
37+
{ method: resources.dag.resolve.parseArgs, assign: 'args' }
38+
],
39+
validate: resources.dag.resolve.validate
40+
},
41+
handler: resources.dag.resolve.handler
42+
}
43+
]

src/http/api/routes/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = [
1919
...require('./pubsub'),
2020
require('./debug'),
2121
...require('./webui'),
22+
...require('./dag'),
2223
require('./dns'),
2324
...require('./key'),
2425
...require('./stats'),

0 commit comments

Comments
 (0)