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