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

Commit 6111234

Browse files
committed
feat: adds http DAG api
1 parent 1478652 commit 6111234

File tree

7 files changed

+395
-3
lines changed

7 files changed

+395
-3
lines changed

src/http/api/resources/dag.js

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

src/http/api/resources/index.js

Lines changed: 1 addition & 0 deletions
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

Lines changed: 43 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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)