hypercore-fetch 8.3.3 → 8.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/index.js +67 -2
- package/package.json +3 -1
- package/test.js +24 -3
package/README.md
CHANGED
|
@@ -130,6 +130,15 @@ The `body` can be either a `String`, an `ArrayBuffer`, a `Blob`, a WHATWG `Reada
|
|
|
130
130
|
|
|
131
131
|
Your `NAME` will likely be a `name` in most cases to ensure you have a writeable archive.
|
|
132
132
|
|
|
133
|
+
### `fetch('hyper://NAME/folder/', {method: 'PUT', body: new FormData()})`
|
|
134
|
+
|
|
135
|
+
You can add multiple files to a folder using the `PUT` method with a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) body.
|
|
136
|
+
|
|
137
|
+
You can [append](https://developer.mozilla.org/en-US/docs/Web/API/FormData) to a FormData with `formData.append(fieldname, content, 'filename.txt')` where `fieldname` gets ignored (use something like `file`?) the `content` can either be a String, Blob, or some sort of stream.
|
|
138
|
+
The `filename` will be the filename within the directory that gets created.
|
|
139
|
+
|
|
140
|
+
`NAME` can either be the 64 character hex key for an archive, a domain to parse with [dat-dns](https://www.npmjs.com/package/dat-dns), or a name for an archive which allows you to write to it.
|
|
141
|
+
|
|
133
142
|
### `fetch('hyper://NAME/example.txt', {method: 'DELETE'})`
|
|
134
143
|
|
|
135
144
|
You can delete a file in an archive by using the `DELETE` method.
|
|
@@ -212,6 +221,10 @@ Using the `text/event-stream` content type in the `Accept` header will get back
|
|
|
212
221
|
|
|
213
222
|
The `event` will be the name of the extension you got the data for, the `id` (accessible by `e.lastEventId` in EventSource) will be set to the ID of the peer that sent it.
|
|
214
223
|
|
|
224
|
+
Only extension messages that have been queried before via a `GET` to the EXTENSION_NAME will be visible in this stream.
|
|
225
|
+
|
|
226
|
+
There are also two special events: `peer-open` which gets emitted whena new peer has connected, and `peer-remove` which gets emitted when an existing peer disconnects.
|
|
227
|
+
|
|
215
228
|
### `fetch('hyper://NAME/$/extensions/EXTENSION_NAME', {method: 'POST', body: 'Example'})`
|
|
216
229
|
|
|
217
230
|
You can broadcast an extension message to all peers that are replicating that extension type with a `POST` to the extension's URL.
|
package/index.js
CHANGED
|
@@ -7,6 +7,8 @@ const makeDir = require('make-dir')
|
|
|
7
7
|
const { Readable } = require('streamx')
|
|
8
8
|
const makeFetch = require('make-fetch')
|
|
9
9
|
const { EventIterator } = require('event-iterator')
|
|
10
|
+
const Busboy = require('busboy')
|
|
11
|
+
const posixPath = require('path').posix
|
|
10
12
|
|
|
11
13
|
const DEFAULT_TIMEOUT = 5000
|
|
12
14
|
|
|
@@ -24,6 +26,8 @@ const TAGS_FOLDER = SPECIAL_FOLDER + TAGS_FOLDER_NAME
|
|
|
24
26
|
const EXTENSIONS_FOLDER_NAME = 'extensions/'
|
|
25
27
|
const EXTENSIONS_FOLDER = SPECIAL_FOLDER + EXTENSIONS_FOLDER_NAME
|
|
26
28
|
const EXTENSION_EVENT = 'extension-message'
|
|
29
|
+
const PEER_OPEN = 'peer-open'
|
|
30
|
+
const PEER_REMOVE = 'peer-remove'
|
|
27
31
|
|
|
28
32
|
// TODO: Add caching support
|
|
29
33
|
const { resolveURL: DEFAULT_RESOLVE_URL } = require('hyper-dns')
|
|
@@ -279,9 +283,23 @@ module.exports = function makeHyperFetch (opts = {}) {
|
|
|
279
283
|
const data = content.split('\n').map((line) => `data:${line}\n`).join('')
|
|
280
284
|
push(`id:${id}\nevent:${name}\n${data}\n`)
|
|
281
285
|
}
|
|
286
|
+
function onPeerOpen (peer) {
|
|
287
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
288
|
+
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
289
|
+
}
|
|
290
|
+
function onPeerRemove (peer) {
|
|
291
|
+
// Whatever, probably an uninitialized peer
|
|
292
|
+
if (!peer.remotePublicKey) return
|
|
293
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
294
|
+
push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
|
|
295
|
+
}
|
|
282
296
|
archive.on(EXTENSION_EVENT, onMessage)
|
|
297
|
+
archive.on(PEER_OPEN, onPeerOpen)
|
|
298
|
+
archive.on(PEER_REMOVE, onPeerRemove)
|
|
283
299
|
return () => {
|
|
284
|
-
archive.removeListener(
|
|
300
|
+
archive.removeListener(EXTENSION_EVENT, onMessage)
|
|
301
|
+
archive.removeListener(PEER_OPEN, onPeerOpen)
|
|
302
|
+
archive.removeListener(PEER_REMOVE, onPeerRemove)
|
|
285
303
|
}
|
|
286
304
|
})
|
|
287
305
|
|
|
@@ -364,9 +382,56 @@ module.exports = function makeHyperFetch (opts = {}) {
|
|
|
364
382
|
|
|
365
383
|
if (method === 'PUT') {
|
|
366
384
|
checkWritable(archive)
|
|
385
|
+
const contentType = headers.get('Content-Type') || headers.get('content-type')
|
|
386
|
+
const isFormData = contentType && contentType.includes('multipart/form-data')
|
|
387
|
+
|
|
367
388
|
if (path.endsWith('/')) {
|
|
368
389
|
await makeDir(path, { fs: archive })
|
|
390
|
+
const busboy = new Busboy({ headers: rawHeaders })
|
|
391
|
+
|
|
392
|
+
const toUpload = new EventIterator(({ push, stop, fail }) => {
|
|
393
|
+
busboy.once('error', fail)
|
|
394
|
+
busboy.once('finish', stop)
|
|
395
|
+
|
|
396
|
+
busboy.on('file', async (fieldName, fileData, fileName) => {
|
|
397
|
+
const finalPath = posixPath.join(path, fileName)
|
|
398
|
+
|
|
399
|
+
const source = Readable.from(fileData)
|
|
400
|
+
const destination = archive.createWriteStream(finalPath)
|
|
401
|
+
|
|
402
|
+
source.pipe(destination)
|
|
403
|
+
try {
|
|
404
|
+
Promise.race([
|
|
405
|
+
once(source, 'error').then((e) => { throw e }),
|
|
406
|
+
once(destination, 'error').then((e) => { throw e }),
|
|
407
|
+
once(source, 'end')
|
|
408
|
+
])
|
|
409
|
+
} catch (e) {
|
|
410
|
+
fail(e)
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
// TODO: Does busboy need to be GC'd?
|
|
415
|
+
return () => {}
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
Readable.from(body).pipe(busboy)
|
|
419
|
+
|
|
420
|
+
await Promise.all(await collect(toUpload))
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
statusCode: 200,
|
|
424
|
+
headers: responseHeaders,
|
|
425
|
+
data: intoAsyncIterable(canonical)
|
|
426
|
+
}
|
|
369
427
|
} else {
|
|
428
|
+
if (isFormData) {
|
|
429
|
+
return {
|
|
430
|
+
statusCode: 400,
|
|
431
|
+
headers: responseHeaders,
|
|
432
|
+
data: intoAsyncIterable('FormData only supported for folders (ending with a /)')
|
|
433
|
+
}
|
|
434
|
+
}
|
|
370
435
|
const parentDir = path.split('/').slice(0, -1).join('/')
|
|
371
436
|
if (parentDir) {
|
|
372
437
|
await makeDir(parentDir, { fs: archive })
|
|
@@ -389,7 +454,7 @@ module.exports = function makeHyperFetch (opts = {}) {
|
|
|
389
454
|
return {
|
|
390
455
|
statusCode: 200,
|
|
391
456
|
headers: responseHeaders,
|
|
392
|
-
data: intoAsyncIterable(
|
|
457
|
+
data: intoAsyncIterable(canonical)
|
|
393
458
|
}
|
|
394
459
|
} else if (method === 'DELETE') {
|
|
395
460
|
if (headers.get('x-clear') === 'cache') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore-fetch",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.6.0",
|
|
4
4
|
"description": "Implementation of Fetch that uses the Dat SDK for loading p2p content",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hypercore-fetch": "bin.js"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"homepage": "https://github.com/RangerMauve/hypercore-fetch#readme",
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"busboy": "^0.3.1",
|
|
27
28
|
"event-iterator": "^2.0.0",
|
|
28
29
|
"fetch-headers": "^2.0.0",
|
|
29
30
|
"hyper-dns": "^0.12.0",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"streamx": "^2.10.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
41
|
+
"form-data": "^4.0.0",
|
|
40
42
|
"random-access-memory": "^3.1.1",
|
|
41
43
|
"tape": "^5.2.2"
|
|
42
44
|
}
|
package/test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const SDK = require('hyper-sdk')
|
|
2
2
|
const test = require('tape')
|
|
3
|
+
const FormData = require('form-data')
|
|
3
4
|
|
|
4
5
|
runTests()
|
|
5
6
|
|
|
@@ -84,10 +85,30 @@ async function runTests () {
|
|
|
84
85
|
t.equal(await response2.text(), SAMPLE_CONTENT, 'Read back written data')
|
|
85
86
|
})
|
|
86
87
|
|
|
87
|
-
test('PUT directory', async (t) => {
|
|
88
|
-
const
|
|
88
|
+
test.only('PUT FormData to directory', async (t) => {
|
|
89
|
+
const form = new FormData()
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
form.append('file', SAMPLE_CONTENT, {
|
|
92
|
+
filename: 'example.txt'
|
|
93
|
+
})
|
|
94
|
+
const body = form.getBuffer()
|
|
95
|
+
const headers = form.getHeaders()
|
|
96
|
+
|
|
97
|
+
const response1 = await fetch('hyper://example/foo/bar/', {
|
|
98
|
+
method: 'PUT',
|
|
99
|
+
headers,
|
|
100
|
+
body
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
t.equal(response1.status, 200, 'Got OK response on directory upload')
|
|
104
|
+
|
|
105
|
+
console.log(await response1.text())
|
|
106
|
+
|
|
107
|
+
const response2 = await fetch('hyper://example/foo/bar/example.txt')
|
|
108
|
+
|
|
109
|
+
t.equal(response2.status, 200, 'Got OK response on read')
|
|
110
|
+
|
|
111
|
+
t.equal(await response2.text(), SAMPLE_CONTENT, 'Read back written data')
|
|
91
112
|
})
|
|
92
113
|
|
|
93
114
|
test('PUT file in new directory', async (t) => {
|