Skip to content

Commit 7685e44

Browse files
authored
feat: support aborting exports (#47)
* feat: support aborting exports Adds a `signal` option to the exporter that will be passed to `ipld.get`, allowing it to be notified that the user is no longer interested in exporting the file/directory/etc. Current behaviour is that if the node is offline and a block is not in the repo is will throw, if it's online the CID will be added to the bitswap want list via the block service. Follow up PRs to ipld, block service and bitswap will impelement the removal logic. * docs: add signal option to readme
1 parent db2c878 commit 7685e44

File tree

12 files changed

+71
-32
lines changed

12 files changed

+71
-32
lines changed

packages/ipfs-unixfs-exporter/README.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
- [Usage](#usage)
2222
- [Example](#example)
2323
- [API](#api)
24-
- [`exporter(cid, ipld)`](#exportercid-ipld)
24+
- [`exporter(cid, ipld, options)`](#exportercid-ipld-options)
2525
- [UnixFS V1 entries](#unixfs-v1-entries)
2626
- [Raw entries](#raw-entries)
2727
- [CBOR entries](#cbor-entries)
@@ -85,12 +85,16 @@ console.info(content) // 0, 1, 2, 3
8585
const exporter = require('ipfs-unixfs-exporter')
8686
```
8787

88-
### `exporter(cid, ipld)`
88+
### `exporter(cid, ipld, options)`
8989

90-
Uses the given [js-ipld instance][] to fetch an IPFS node by it's CID.
90+
Uses the given [ipld](https://github.com/ipld/js-ipld) instance to fetch an IPFS node by it's CID.
9191

9292
Returns a Promise which resolves to an `entry`.
9393

94+
`options` is an optional object argument that might include the following keys:
95+
96+
- `signal` ([AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)): Used to cancel any network requests that are initiated as a result of this export
97+
9498
#### UnixFS V1 entries
9599

96100
Entries with a `dag-pb` codec `CID` return UnixFS V1 entries:

packages/ipfs-unixfs-exporter/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
},
3636
"homepage": "https://github.com/ipfs/js-ipfs-unixfs#readme",
3737
"devDependencies": {
38+
"abort-controller": "^3.0.0",
3839
"aegir": "^21.3.0",
3940
"async-iterator-all": "^1.0.0",
4041
"async-iterator-buffer-stream": "^1.0.0",
4142
"async-iterator-first": "^1.0.0",
4243
"chai": "^4.2.0",
44+
"chai-as-promised": "^7.1.1",
4345
"detect-node": "^2.0.4",
4446
"dirty-chai": "^2.0.1",
4547
"ipfs-unixfs-importer": "^1.0.3",

packages/ipfs-unixfs-exporter/src/index.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const cidAndRest = (path) => {
4444
throw errCode(new Error(`Unknown path type ${path}`), 'ERR_BAD_PATH')
4545
}
4646

47-
const walkPath = async function * (path, ipld) {
47+
const walkPath = async function * (path, ipld, options) {
4848
let {
4949
cid,
5050
toResolve
@@ -54,7 +54,7 @@ const walkPath = async function * (path, ipld) {
5454
const startingDepth = toResolve.length
5555

5656
while (true) {
57-
const result = await resolve(cid, name, entryPath, toResolve, startingDepth, ipld)
57+
const result = await resolve(cid, name, entryPath, toResolve, startingDepth, ipld, options)
5858

5959
if (!result.entry && !result.next) {
6060
throw errCode(new Error(`Could not resolve ${path}`), 'ERR_NOT_FOUND')
@@ -76,27 +76,27 @@ const walkPath = async function * (path, ipld) {
7676
}
7777
}
7878

79-
const exporter = (path, ipld) => {
80-
return last(walkPath(path, ipld))
79+
const exporter = (path, ipld, options) => {
80+
return last(walkPath(path, ipld, options))
8181
}
8282

83-
const recursive = async function * (path, ipld) {
84-
const node = await exporter(path, ipld)
83+
const recursive = async function * (path, ipld, options) {
84+
const node = await exporter(path, ipld, options)
8585

8686
yield node
8787

8888
if (node.unixfs && node.unixfs.type.includes('dir')) {
89-
for await (const child of recurse(node)) {
89+
for await (const child of recurse(node, options)) {
9090
yield child
9191
}
9292
}
9393

94-
async function * recurse (node) {
95-
for await (const file of node.content()) {
94+
async function * recurse (node, options) {
95+
for await (const file of node.content(options)) {
9696
yield file
9797

9898
if (file.unixfs.type.includes('dir')) {
99-
for await (const subFile of recurse(file)) {
99+
for await (const subFile of recurse(file, options)) {
100100
yield subFile
101101
}
102102
}

packages/ipfs-unixfs-exporter/src/resolvers/dag-cbor.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
const CID = require('cids')
44
const errCode = require('err-code')
55

6-
const resolve = async (cid, name, path, toResolve, resolve, depth, ipld) => {
7-
const node = await ipld.get(cid)
6+
const resolve = async (cid, name, path, toResolve, resolve, depth, ipld, options) => {
7+
const node = await ipld.get(cid, options)
88
let subObject = node
99
let subPath = path
1010

packages/ipfs-unixfs-exporter/src/resolvers/identity.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const rawContent = (node) => {
1616
}
1717
}
1818

19-
const resolve = async (cid, name, path, toResolve, resolve, depth, ipld) => {
19+
const resolve = async (cid, name, path, toResolve, resolve, depth, ipld, options) => {
2020
if (toResolve.length) {
2121
throw errCode(new Error(`No link named ${path} found in raw node ${cid.toBaseEncodedString()}`), 'ERR_NOT_FOUND')
2222
}

packages/ipfs-unixfs-exporter/src/resolvers/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ const resolvers = {
99
identity: require('./identity')
1010
}
1111

12-
const resolve = (cid, name, path, toResolve, depth, ipld) => {
12+
const resolve = (cid, name, path, toResolve, depth, ipld, options) => {
1313
const resolver = resolvers[cid.codec]
1414

1515
if (!resolver) {
1616
throw errCode(new Error(`No resolver for codec ${cid.codec}`), 'ERR_NO_RESOLVER')
1717
}
1818

19-
return resolver(cid, name, path, toResolve, resolve, depth, ipld)
19+
return resolver(cid, name, path, toResolve, resolve, depth, ipld, options)
2020
}
2121

2222
module.exports = resolve

packages/ipfs-unixfs-exporter/src/resolvers/raw.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ const rawContent = (node) => {
1515
}
1616
}
1717

18-
const resolve = async (cid, name, path, toResolve, resolve, depth, ipld) => {
18+
const resolve = async (cid, name, path, toResolve, resolve, depth, ipld, options) => {
1919
if (toResolve.length) {
2020
throw errCode(new Error(`No link named ${path} found in raw node ${cid.toBaseEncodedString()}`), 'ERR_NOT_FOUND')
2121
}
2222

23-
const buf = await ipld.get(cid)
23+
const buf = await ipld.get(cid, options)
2424

2525
return {
2626
entry: {

packages/ipfs-unixfs-exporter/src/resolvers/unixfs-v1/content/directory.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
'use strict'
22

3-
const directoryContent = (cid, node, unixfs, path, resolve, depth, ipld) => {
3+
const directoryContent = (cid, node, unixfs, path, resolve, depth, ipld, options) => {
44
return async function * (options = {}) {
55
const offset = options.offset || 0
66
const length = options.length || node.Links.length
77
const links = node.Links.slice(offset, length)
88

99
for (const link of links) {
10-
const result = await resolve(link.Hash, link.Name, `${path}/${link.Name}`, [], depth + 1, ipld)
10+
const result = await resolve(link.Hash, link.Name, `${path}/${link.Name}`, [], depth + 1, ipld, options)
1111

1212
yield result.entry
1313
}

packages/ipfs-unixfs-exporter/src/resolvers/unixfs-v1/content/file.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const validateOffsetAndLength = require('../../../utils/validate-offset-and-leng
55
const UnixFS = require('ipfs-unixfs')
66
const errCode = require('err-code')
77

8-
async function * emitBytes (ipld, node, start, end, streamPosition = 0) {
8+
async function * emitBytes (ipld, node, start, end, streamPosition = 0, options) {
99
// a `raw` node
1010
if (Buffer.isBuffer(node)) {
1111
const buf = extractDataFromBlock(node, streamPosition, start, end)
@@ -50,9 +50,9 @@ async function * emitBytes (ipld, node, start, end, streamPosition = 0) {
5050
if ((start >= childStart && start < childEnd) || // child has offset byte
5151
(end > childStart && end <= childEnd) || // child has end byte
5252
(start < childStart && end > childEnd)) { // child is between offset and end bytes
53-
const child = await ipld.get(childLink.Hash)
53+
const child = await ipld.get(childLink.Hash, options)
5454

55-
for await (const buf of emitBytes(ipld, child, start, end, streamPosition)) {
55+
for await (const buf of emitBytes(ipld, child, start, end, streamPosition, options)) {
5656
streamPosition += buf.length
5757

5858
yield buf
@@ -76,7 +76,7 @@ const fileContent = (cid, node, unixfs, path, resolve, depth, ipld) => {
7676
const start = offset
7777
const end = offset + length
7878

79-
return emitBytes(ipld, node, start, end)
79+
return emitBytes(ipld, node, start, end, 0, options)
8080
}
8181
}
8282

packages/ipfs-unixfs-exporter/src/resolvers/unixfs-v1/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const contentExporters = {
1919
symlink: (cid, node, unixfs, path, resolve, depth, ipld) => {}
2020
}
2121

22-
const unixFsResolver = async (cid, name, path, toResolve, resolve, depth, ipld) => {
23-
const node = await ipld.get(cid)
22+
const unixFsResolver = async (cid, name, path, toResolve, resolve, depth, ipld, options) => {
23+
const node = await ipld.get(cid, options)
2424
let unixfs
2525
let next
2626

@@ -71,7 +71,7 @@ const unixFsResolver = async (cid, name, path, toResolve, resolve, depth, ipld)
7171
path,
7272
cid,
7373
node,
74-
content: contentExporters[unixfs.type](cid, node, unixfs, path, resolve, depth, ipld),
74+
content: contentExporters[unixfs.type](cid, node, unixfs, path, resolve, depth, ipld, options),
7575
unixfs,
7676
depth
7777
},

packages/ipfs-unixfs-exporter/src/utils/find-cid-in-shard.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const toBucketPath = (position) => {
6262
return path.reverse()
6363
}
6464

65-
const findShardCid = async (node, name, ipld, context) => {
65+
const findShardCid = async (node, name, ipld, context, options) => {
6666
if (!context) {
6767
context = {
6868
rootBucket: new Bucket({
@@ -113,9 +113,9 @@ const findShardCid = async (node, name, ipld, context) => {
113113

114114
context.hamtDepth++
115115

116-
node = await ipld.get(link.Hash)
116+
node = await ipld.get(link.Hash, options)
117117

118-
return findShardCid(node, name, ipld, context)
118+
return findShardCid(node, name, ipld, context, options)
119119
}
120120

121121
module.exports = findShardCid

packages/ipfs-unixfs-exporter/test/exporter.spec.js

+33
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
const chai = require('chai')
55
chai.use(require('dirty-chai'))
6+
chai.use(require('chai-as-promised'))
67
const expect = chai.expect
78
const IPLD = require('ipld')
89
const inMemory = require('ipld-in-memory')
@@ -20,6 +21,7 @@ const all = require('async-iterator-all')
2021
const last = require('it-last')
2122
const first = require('async-iterator-first')
2223
const randomBytes = require('async-iterator-buffer-stream')
24+
const AbortController = require('abort-controller')
2325

2426
const ONE_MEG = Math.pow(1024, 2)
2527

@@ -953,4 +955,35 @@ describe('exporter', () => {
953955

954956
expect(result.toString('utf8')).to.equal('l')
955957
})
958+
959+
it('aborts a request', async () => {
960+
const abortController = new AbortController()
961+
962+
// data should not be in IPLD
963+
const data = Buffer.from(`hello world '${Math.random()}`)
964+
const hash = mh.encode(data, 'sha2-256')
965+
const cid = new CID(1, 'dag-pb', hash)
966+
const message = `User aborted ${Math.random()}`
967+
968+
setTimeout(() => {
969+
abortController.abort()
970+
}, 100)
971+
972+
// regular test IPLD is offline-only, we need to mimic what happens when
973+
// we try to get a block from the network
974+
const ipld = {
975+
get: (cid, options) => {
976+
// promise will never resolve, so reject it when the abort signal is sent
977+
return new Promise((resolve, reject) => {
978+
options.signal.addEventListener('abort', () => {
979+
reject(new Error(message))
980+
})
981+
})
982+
}
983+
}
984+
985+
await expect(exporter(cid, ipld, {
986+
signal: abortController.signal
987+
})).to.eventually.be.rejectedWith(message)
988+
})
956989
})

0 commit comments

Comments
 (0)