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

Commit 015ab10

Browse files
committed
Merge pull request #75 from ipfs/feature/http-api-config-replace
Add /api/v0/config/replace endpoint
2 parents 05f2549 + 6b0838b commit 015ab10

File tree

6 files changed

+242
-0
lines changed

6 files changed

+242
-0
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"buffer-loader": "0.0.1",
4343
"chai": "^3.4.1",
4444
"expose-loader": "^0.7.1",
45+
"form-data": "^1.0.0-rc3",
4546
"fs-blob-store": "^5.2.1",
4647
"idb-plus-blob-store": "^1.0.0",
4748
"istanbul": "^0.4.1",
@@ -61,6 +62,7 @@
6162
"pre-commit": "^1.1.2",
6263
"rimraf": "^2.4.4",
6364
"standard": "^5.4.1",
65+
"stream-to-promise": "^1.1.0",
6466
"transform-loader": "^0.2.3",
6567
"webpack": "^2.0.7-beta"
6668
},
@@ -73,6 +75,7 @@
7375
"ipfs-api": "^2.13.1",
7476
"ipfs-blocks": "^0.1.0",
7577
"ipfs-merkle-dag": "^0.2.1",
78+
"ipfs-multipart": "0.0.1",
7679
"ipfs-repo": "^0.5.0",
7780
"joi": "^8.0.2",
7881
"lodash.get": "^4.0.0",

src/http-api/resources/config.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
'use strict'
2+
13
const ipfs = require('./../index.js').ipfs
24
const debug = require('debug')
35
const get = require('lodash.get')
46
const set = require('lodash.set')
57
const log = debug('http-api:config')
68
log.error = debug('http-api:config:error')
9+
const multipart = require('ipfs-multipart')
710

811
exports = module.exports
912

@@ -124,3 +127,61 @@ exports.show = (request, reply) => {
124127
return reply(config)
125128
})
126129
}
130+
131+
exports.replace = {
132+
// pre request handler that parses the args and returns `config` which is assigned to `request.pre.args`
133+
parseArgs: (request, reply) => {
134+
if (!request.payload) {
135+
return reply({
136+
Message: "Argument 'file' is required",
137+
Code: 1123
138+
139+
}).code(400).takeover()
140+
}
141+
142+
const parser = multipart.reqParser(request.payload)
143+
let file
144+
145+
parser.on('file', (fileName, fileStream) => {
146+
fileStream.on('data', (data) => {
147+
file = data
148+
})
149+
})
150+
151+
parser.on('end', () => {
152+
if (!file) {
153+
return reply({
154+
Message: "Argument 'file' is required",
155+
Code: 1123
156+
157+
}).code(400).takeover()
158+
}
159+
160+
try {
161+
return reply({
162+
config: JSON.parse(file.toString())
163+
})
164+
} catch (err) {
165+
return reply({
166+
Message: 'Failed to decode file as config: ' + err,
167+
Code: 0
168+
}).code(500).takeover()
169+
}
170+
})
171+
},
172+
173+
// main route handler which is called after the above `parseArgs`, but only if the args were valid
174+
handler: (request, reply) => {
175+
return ipfs.config.replace(request.pre.args.config, (err) => {
176+
if (err) {
177+
log.error(err)
178+
return reply({
179+
Message: 'Failed to save config: ' + err,
180+
Code: 0
181+
}).code(500)
182+
}
183+
184+
return reply()
185+
})
186+
}
187+
}

src/http-api/routes/config.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,18 @@ api.route({
1717
path: '/api/v0/config/show',
1818
handler: resources.config.show
1919
})
20+
21+
api.route({
22+
method: '*',
23+
path: '/api/v0/config/replace',
24+
config: {
25+
payload: {
26+
parse: false,
27+
output: 'stream'
28+
},
29+
pre: [
30+
{ method: resources.config.replace.parseArgs, assign: 'args' }
31+
],
32+
handler: resources.config.replace.handler
33+
}
34+
})

tests/badconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
bad config
3+
}

tests/otherconfig

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"Identity": {
3+
"PeerID": "QmQ2zigjQikYnyYUSXZydNXrDRhBut2mubwJBaLXobMt3A",
4+
"PrivKey": "CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw=="
5+
},
6+
"Datastore": {
7+
"Type": "",
8+
"Path": "",
9+
"StorageMax": "",
10+
"StorageGCWatermark": 0,
11+
"GCPeriod": "",
12+
"Params": null,
13+
"NoSync": false
14+
},
15+
"Addresses": {
16+
"Swarm": ["/ip4/0.0.0.0/tcp/4001", "/ip6/::/tcp/4001"],
17+
"API": "/ip4/127.0.0.1/tcp/6001",
18+
"Gateway": "/ip4/127.0.0.1/tcp/9090"
19+
},
20+
"Mounts": {
21+
"IPFS": "/ipfs",
22+
"IPNS": "/ipns",
23+
"FuseAllowOther": false
24+
},
25+
"Version": {
26+
"Current": "0.4.0-dev",
27+
"Check": "error",
28+
"CheckDate": "0001-01-01T00:00:00Z",
29+
"CheckPeriod": "172800000000000",
30+
"AutoUpdate": "minor"
31+
},
32+
"Discovery": {
33+
"MDNS": {
34+
"Enabled": true,
35+
"Interval": 10
36+
}
37+
},
38+
"Ipns": {
39+
"RepublishPeriod": "",
40+
"RecordLifetime": "",
41+
"ResolveCacheSize": 128
42+
},
43+
"Bootstrap": ["/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", "/ip4/104.236.176.52/tcp/4001/ipfs/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z", "/ip4/104.236.179.241/tcp/4001/ipfs/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM", "/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm", "/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu", "/ip4/104.236.76.40/tcp/4001/ipfs/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64", "/ip4/178.62.158.247/tcp/4001/ipfs/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd", "/ip4/178.62.61.185/tcp/4001/ipfs/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3", "/ip4/104.236.151.122/tcp/4001/ipfs/QmSoLju6m7xTh3DuokvT3886QRYqxAzb1kShaanJgW36yx"],
44+
"Tour": {
45+
"Last": ""
46+
},
47+
"Gateway": {
48+
"HTTPHeaders": null,
49+
"RootRedirect": "",
50+
"Writable": false
51+
},
52+
"SupernodeRouting": {
53+
"Servers": ["/ip4/104.236.176.52/tcp/4002/ipfs/QmXdb7tWTxdFEQEFgWBqkuYSrZd3mXrC7HxkD4krGNYx2U", "/ip4/104.236.179.241/tcp/4002/ipfs/QmVRqViDByUxjUMoPnjurjKvZhaEMFDtK35FJXHAM4Lkj6", "/ip4/104.236.151.122/tcp/4002/ipfs/QmSZwGx8Tn8tmcM4PtDJaMeUQNRhNFdBLVGPzRiNaRJtFH", "/ip4/162.243.248.213/tcp/4002/ipfs/QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP", "/ip4/128.199.219.111/tcp/4002/ipfs/Qmb3brdCYmKG1ycwqCbo6LUwWxTuo3FisnJV2yir7oN92R", "/ip4/104.236.76.40/tcp/4002/ipfs/QmdRBCV8Cz2dGhoKLkD3YjPwVFECmqADQkx5ZteF2c6Fy4", "/ip4/178.62.158.247/tcp/4002/ipfs/QmUdiMPci7YoEUBkyFZAh2pAbjqcPr7LezyiPD2artLw3v", "/ip4/178.62.61.185/tcp/4002/ipfs/QmVw6fGNqBixZE4bewRLT2VXX7fAHUHs8JyidDiJ1P7RUN"]
54+
},
55+
"API": {
56+
"HTTPHeaders": {
57+
"Access-Control-Allow-Origin": [
58+
"http://example.com"
59+
]
60+
}
61+
},
62+
"Swarm": {
63+
"AddrFilters": null
64+
},
65+
"Log": {
66+
"MaxSizeMB": 250,
67+
"MaxBackups": 1,
68+
"MaxAgeDays": 0
69+
}
70+
}

tests/test-http-api/test-config.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/* eslint-env mocha */
2+
'use strict'
23

34
const expect = require('chai').expect
45
const fs = require('fs')
56
const APIctl = require('ipfs-api')
7+
const FormData = require('form-data')
8+
const streamToPromise = require('stream-to-promise')
69

710
describe('config', () => {
811
const configPath = process.cwd() + '/tests/repo-tests-run/config'
12+
const originalConfigPath = process.cwd() + '/tests/repo-example/config'
913
const updatedConfig = () => JSON.parse(fs.readFileSync(configPath, 'utf8'))
14+
const restoreConfig = () => fs.writeFileSync(configPath, fs.readFileSync(originalConfigPath, 'utf8'), 'utf8')
1015

1116
describe('api', () => {
1217
var api
@@ -143,6 +148,69 @@ describe('config', () => {
143148
done()
144149
})
145150
})
151+
152+
describe('/config/replace', () => {
153+
it('returns 400 if no config is provided', (done) => {
154+
const form = new FormData()
155+
const headers = form.getHeaders()
156+
157+
streamToPromise(form).then(payload => {
158+
api.inject({
159+
method: 'POST',
160+
url: '/api/v0/config/replace',
161+
headers: headers,
162+
payload: payload
163+
}, res => {
164+
expect(res.statusCode).to.equal(400)
165+
done()
166+
})
167+
})
168+
})
169+
170+
it('returns 500 if the config is invalid', (done) => {
171+
const form = new FormData()
172+
const filePath = 'tests/badconfig'
173+
form.append('file', fs.createReadStream(filePath))
174+
const headers = form.getHeaders()
175+
176+
streamToPromise(form).then(payload => {
177+
api.inject({
178+
method: 'POST',
179+
url: '/api/v0/config/replace',
180+
headers: headers,
181+
payload: payload
182+
}, res => {
183+
expect(res.statusCode).to.equal(500)
184+
done()
185+
})
186+
})
187+
})
188+
189+
it('updates value', (done) => {
190+
const form = new FormData()
191+
const filePath = 'tests/otherconfig'
192+
form.append('file', fs.createReadStream(filePath))
193+
const headers = form.getHeaders()
194+
const expectedConfig = JSON.parse(fs.readFileSync(filePath, 'utf8'))
195+
196+
streamToPromise(form).then(payload => {
197+
api.inject({
198+
method: 'POST',
199+
url: '/api/v0/config/replace',
200+
headers: headers,
201+
payload: payload
202+
}, res => {
203+
expect(res.statusCode).to.equal(200)
204+
expect(updatedConfig()).to.deep.equal(expectedConfig)
205+
done()
206+
})
207+
})
208+
})
209+
210+
after(() => {
211+
restoreConfig()
212+
})
213+
})
146214
})
147215

148216
describe('using js-ipfs-api', () => {
@@ -240,5 +308,27 @@ describe('config', () => {
240308
done()
241309
})
242310
})
311+
312+
describe('ipfs.config.replace', () => {
313+
it('returns error if the config is invalid', (done) => {
314+
const filePath = 'tests/badconfig'
315+
316+
ctl.config.replace(filePath, (err) => {
317+
expect(err).to.exist
318+
done()
319+
})
320+
})
321+
322+
it('updates value', (done) => {
323+
const filePath = 'tests/otherconfig'
324+
const expectedConfig = JSON.parse(fs.readFileSync(filePath, 'utf8'))
325+
326+
ctl.config.replace(filePath, (err) => {
327+
expect(err).not.to.exist
328+
expect(expectedConfig).to.deep.equal(updatedConfig())
329+
done()
330+
})
331+
})
332+
})
243333
})
244334
})

0 commit comments

Comments
 (0)