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

Commit c9be79e

Browse files
authored
feat: support block.rm over http api (#2514)
* feat: support block.rm over http api Also, validate that a block is not pinned before removing it. * chore: skip config endpoint tests * chore: fix up interface tests * chore: rev http client * test: add more cli tests * chore: update deps * chore: give dep correct name * chore: fix failing test * chore: address PR comments * chore: update interop dep
1 parent eefb10f commit c9be79e

File tree

9 files changed

+199
-64
lines changed

9 files changed

+199
-64
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"ipfs-bitswap": "^0.26.0",
100100
"ipfs-block": "~0.8.1",
101101
"ipfs-block-service": "~0.16.0",
102-
"ipfs-http-client": "^38.1.0",
102+
"ipfs-http-client": "^38.2.0",
103103
"ipfs-http-response": "~0.3.1",
104104
"ipfs-mfs": "^0.13.0",
105105
"ipfs-multipart": "^0.2.0",
@@ -204,7 +204,7 @@
204204
"form-data": "^2.5.1",
205205
"hat": "0.0.3",
206206
"interface-ipfs-core": "^0.117.2",
207-
"ipfs-interop": "~0.1.0",
207+
"ipfs-interop": "^0.1.1",
208208
"ipfsd-ctl": "^0.47.2",
209209
"libp2p-websocket-star": "~0.10.2",
210210
"ncp": "^2.0.0",

src/cli/commands/block/rm.js

+34-10
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
11
'use strict'
22

33
module.exports = {
4-
command: 'rm <key>',
4+
command: 'rm <hash...>',
55

6-
describe: 'Remove a raw IPFS block',
6+
describe: 'Remove IPFS block(s)',
77

8-
builder: {},
8+
builder: {
9+
force: {
10+
alias: 'f',
11+
describe: 'Ignore nonexistent blocks',
12+
type: 'boolean',
13+
default: false
14+
},
15+
quiet: {
16+
alias: 'q',
17+
describe: 'Write minimal output',
18+
type: 'boolean',
19+
default: false
20+
}
21+
},
922

10-
handler ({ getIpfs, print, isDaemonOn, key, resolve }) {
23+
handler ({ getIpfs, print, hash, force, quiet, resolve }) {
1124
resolve((async () => {
12-
if (isDaemonOn()) {
13-
// TODO implement this once `js-ipfs-http-client` supports it
14-
throw new Error('rm block with daemon running is not yet implemented')
25+
const ipfs = await getIpfs()
26+
let errored = false
27+
28+
for await (const result of ipfs.block._rmAsyncIterator(hash, {
29+
force,
30+
quiet
31+
})) {
32+
if (result.error) {
33+
errored = true
34+
}
35+
36+
if (!quiet) {
37+
print(result.error || 'removed ' + result.hash)
38+
}
1539
}
1640

17-
const ipfs = await getIpfs()
18-
await ipfs.block.rm(key)
19-
print('removed ' + key)
41+
if (errored && !quiet) {
42+
throw new Error('some blocks not removed')
43+
}
2044
})())
2145
}
2246
}

src/core/components/block.js

+65-29
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,67 @@ const multihashing = require('multihashing-async')
55
const CID = require('cids')
66
const callbackify = require('callbackify')
77
const errCode = require('err-code')
8+
const all = require('async-iterator-all')
9+
const { PinTypes } = require('./pin/pin-manager')
810

911
module.exports = function block (self) {
10-
return {
11-
get: callbackify.variadic(async (cid, options) => { // eslint-disable-line require-await
12-
options = options || {}
12+
async function * rmAsyncIterator (cids, options) {
13+
options = options || {}
1314

14-
try {
15+
if (!Array.isArray(cids)) {
16+
cids = [cids]
17+
}
18+
19+
// We need to take a write lock here to ensure that adding and removing
20+
// blocks are exclusive operations
21+
const release = await self._gcLock.writeLock()
22+
23+
try {
24+
for (let cid of cids) {
1525
cid = cleanCid(cid)
16-
} catch (err) {
17-
throw errCode(err, 'ERR_INVALID_CID')
26+
27+
const result = {
28+
hash: cid.toString()
29+
}
30+
31+
try {
32+
const pinResult = await self.pin.pinManager.isPinnedWithType(cid, PinTypes.all)
33+
34+
if (pinResult.pinned) {
35+
if (CID.isCID(pinResult.reason)) { // eslint-disable-line max-depth
36+
throw errCode(new Error(`pinned via ${pinResult.reason}`))
37+
}
38+
39+
throw errCode(new Error(`pinned: ${pinResult.reason}`))
40+
}
41+
42+
// remove has check when https://github.com/ipfs/js-ipfs-block-service/pull/88 is merged
43+
const has = await self._blockService._repo.blocks.has(cid)
44+
45+
if (!has) {
46+
throw errCode(new Error('block not found'), 'ERR_BLOCK_NOT_FOUND')
47+
}
48+
49+
await self._blockService.delete(cid)
50+
} catch (err) {
51+
if (!options.force) {
52+
result.error = `cannot remove ${cid}: ${err.message}`
53+
}
54+
}
55+
56+
if (!options.quiet) {
57+
yield result
58+
}
1859
}
60+
} finally {
61+
release()
62+
}
63+
}
64+
65+
return {
66+
get: callbackify.variadic(async (cid, options) => { // eslint-disable-line require-await
67+
options = options || {}
68+
cid = cleanCid(cid)
1969

2070
if (options.preload !== false) {
2171
self._preload(cid)
@@ -66,31 +116,13 @@ module.exports = function block (self) {
66116
release()
67117
}
68118
}),
69-
rm: callbackify(async (cid) => {
70-
try {
71-
cid = cleanCid(cid)
72-
} catch (err) {
73-
throw errCode(err, 'ERR_INVALID_CID')
74-
}
75-
76-
// We need to take a write lock here to ensure that adding and removing
77-
// blocks are exclusive operations
78-
const release = await self._gcLock.writeLock()
79-
80-
try {
81-
await self._blockService.delete(cid)
82-
} finally {
83-
release()
84-
}
119+
rm: callbackify.variadic(async (cids, options) => { // eslint-disable-line require-await
120+
return all(rmAsyncIterator(cids, options))
85121
}),
122+
_rmAsyncIterator: rmAsyncIterator,
86123
stat: callbackify.variadic(async (cid, options) => {
87124
options = options || {}
88-
89-
try {
90-
cid = cleanCid(cid)
91-
} catch (err) {
92-
throw errCode(err, 'ERR_INVALID_CID')
93-
}
125+
cid = cleanCid(cid)
94126

95127
if (options.preload !== false) {
96128
self._preload(cid)
@@ -112,5 +144,9 @@ function cleanCid (cid) {
112144
}
113145

114146
// CID constructor knows how to do the cleaning :)
115-
return new CID(cid)
147+
try {
148+
return new CID(cid)
149+
} catch (err) {
150+
throw errCode(err, 'ERR_INVALID_CID')
151+
}
116152
}

src/http/api/resources/block.js

+37-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const Boom = require('@hapi/boom')
88
const { cidToString } = require('../../../utils/cid')
99
const debug = require('debug')
1010
const all = require('async-iterator-all')
11+
const streamResponse = require('../../utils/stream-response')
1112
const log = debug('ipfs:http-api:block')
1213
log.error = debug('ipfs:http-api:block:error')
1314

@@ -102,20 +103,48 @@ exports.put = {
102103
}
103104

104105
exports.rm = {
105-
// uses common parseKey method that returns a `key`
106-
parseArgs: exports.parseKey,
106+
validate: {
107+
query: Joi.object().keys({
108+
arg: Joi.array().items(Joi.string()).single().required(),
109+
force: Joi.boolean().default(false),
110+
quiet: Joi.boolean().default(false)
111+
}).unknown()
112+
},
107113

108-
// main route handler which is called after the above `parseArgs`, but only if the args were valid
109-
async handler (request, h) {
110-
const { key } = request.pre.args
114+
parseArgs: (request, h) => {
115+
let { arg } = request.query
111116

112117
try {
113-
await request.server.app.ipfs.block.rm(key)
118+
arg = arg.map(thing => new CID(thing))
114119
} catch (err) {
115-
throw Boom.boomify(err, { message: 'Failed to delete block' })
120+
throw Boom.badRequest('Not a valid hash')
121+
}
122+
123+
return {
124+
...request.query,
125+
arg
116126
}
127+
},
117128

118-
return h.response()
129+
// main route handler which is called after the above `parseArgs`, but only if the args were valid
130+
handler (request, h) {
131+
const { arg, force, quiet } = request.pre.args
132+
133+
return streamResponse(request, h, async (output) => {
134+
try {
135+
for await (const result of request.server.app.ipfs.block._rmAsyncIterator(arg, {
136+
force,
137+
quiet
138+
})) {
139+
output.write(JSON.stringify({
140+
Hash: result.hash,
141+
Error: result.error
142+
}) + '\n')
143+
}
144+
} catch (err) {
145+
throw Boom.boomify(err, { message: 'Failed to delete block' })
146+
}
147+
})
119148
}
120149
}
121150

src/http/api/routes/block.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ module.exports = [
3131
{
3232
method: '*',
3333
path: '/api/v0/block/rm',
34-
config: {
34+
options: {
3535
pre: [
3636
{ method: resources.block.rm.parseArgs, assign: 'args' }
37-
]
37+
],
38+
validate: resources.block.rm.validate
3839
},
3940
handler: resources.block.rm.handler
4041
},

src/http/utils/stream-response.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict'
2+
3+
const { PassThrough } = require('readable-stream')
4+
5+
function streamResponse (request, h, fn) {
6+
const output = new PassThrough()
7+
const errorTrailer = 'X-Stream-Error'
8+
9+
Promise.resolve()
10+
.then(() => fn(output))
11+
.catch(err => {
12+
request.raw.res.addTrailers({
13+
[errorTrailer]: JSON.stringify({
14+
Message: err.message,
15+
Code: 0
16+
})
17+
})
18+
})
19+
.finally(() => {
20+
output.end()
21+
})
22+
23+
return h.response(output)
24+
.header('x-chunked-output', '1')
25+
.header('content-type', 'application/json')
26+
.header('Trailer', errorTrailer)
27+
}
28+
29+
module.exports = streamResponse

test/cli/block.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,36 @@ describe('block', () => runOnAndOff((thing) => {
7272
].join('\n') + '\n')
7373
})
7474

75-
it.skip('rm', async function () {
75+
it('rm', async function () {
7676
this.timeout(40 * 1000)
7777

78+
await ipfs('block put test/fixtures/test-data/hello')
79+
7880
const out = await ipfs('block rm QmZjTnYw2TFhn9Nn7tjmPSoTBoY7YRkwPzwSrSbabY24Kp')
7981
expect(out).to.eql('removed QmZjTnYw2TFhn9Nn7tjmPSoTBoY7YRkwPzwSrSbabY24Kp\n')
8082
})
83+
84+
it('rm quietly', async function () {
85+
this.timeout(40 * 1000)
86+
87+
await ipfs('block put test/fixtures/test-data/hello')
88+
89+
const out = await ipfs('block rm --quiet QmZjTnYw2TFhn9Nn7tjmPSoTBoY7YRkwPzwSrSbabY24Kp')
90+
expect(out).to.eql('')
91+
})
92+
93+
it('rm force', async function () {
94+
this.timeout(40 * 1000)
95+
96+
const out = await ipfs('block rm --force QmZjTnYw2TFhn9Nn7tjmPSoTBoY7YRkwPzwSrSbabY24Kh')
97+
expect(out).to.eql('removed QmZjTnYw2TFhn9Nn7tjmPSoTBoY7YRkwPzwSrSbabY24Kh\n')
98+
})
99+
100+
it('fails to remove non-existent block', async function () {
101+
this.timeout(40 * 1000)
102+
103+
const out = await ipfs.fail('block rm QmZjTnYw2TFhn9Nn7tjmPSoTBoY7YRkwPzwSrSbabY24Kh')
104+
expect(out.stdout).to.include('block not found')
105+
expect(out.stdout).to.include('some blocks not removed')
106+
})
81107
}))

test/core/interface.spec.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@ describe('interface-ipfs-core tests', function () {
1212

1313
tests.bitswap(defaultCommonFactory, { skip: !isNode })
1414

15-
tests.block(defaultCommonFactory, {
16-
skip: [{
17-
name: 'rm',
18-
reason: 'Not implemented'
19-
}]
20-
})
15+
tests.block(defaultCommonFactory)
2116

2217
tests.bootstrap(defaultCommonFactory)
2318

test/http-api/interface.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@ describe('interface-ipfs-core over ipfs-http-client tests', () => {
1212

1313
tests.bitswap(defaultCommonFactory)
1414

15-
tests.block(defaultCommonFactory, {
16-
skip: [{
17-
name: 'rm',
18-
reason: 'Not implemented'
19-
}]
20-
})
15+
tests.block(defaultCommonFactory)
2116

2217
tests.bootstrap(defaultCommonFactory)
2318

0 commit comments

Comments
 (0)