hypercore-fetch 9.0.7 → 9.1.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 +12 -0
- package/index.js +258 -186
- package/package.json +2 -2
- package/test.js +47 -0
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ Each response will contain a header for the canonical URL represented as a `Link
|
|
|
48
48
|
|
|
49
49
|
There is also an `ETag` header which will be a JSON string containging the drive's current `version`, or the file's sequence number.
|
|
50
50
|
This will change only when the drive has gotten an update of some sort and is monotonically incrementing.
|
|
51
|
+
The `ETag` representing a file's sequence number represents the version the Hyperdrive was at when the file was added.
|
|
52
|
+
Thus you can get the previous version of a file by using `hyper://NAME/$/version/${ETAG}/example.txt`.
|
|
51
53
|
|
|
52
54
|
If the resource is a file, it may contain the `Last-Modified` header if the file has had a `metadata.mtime` flag set upon update.
|
|
53
55
|
|
|
@@ -188,3 +190,13 @@ The `body` of the request will be used as the payload.
|
|
|
188
190
|
Please note that only utf8 encoded text is currently supported due to limitations of the event-stream encoding.
|
|
189
191
|
|
|
190
192
|
Note that this requires the `extensionMessages: true` flag.
|
|
193
|
+
|
|
194
|
+
### `fetch('hyper://NAME/$/version/VERSION_NUMBER/example.txt')`
|
|
195
|
+
|
|
196
|
+
You can get older views of data in an archive by using the special `/$/version` folder with a version number to view older states.
|
|
197
|
+
|
|
198
|
+
`VERSION_NUMBER` should be a number representing the version to check out based on the `ETag` of the root of the archive.
|
|
199
|
+
|
|
200
|
+
From there, you can use `GET` and `HEAD` requests with allt he same headers and querystring paramters as non-versioned paths to data.
|
|
201
|
+
|
|
202
|
+
Note that you cannot `PUT` or `DELETE` data in a versioned folder.
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const SPECIAL_DOMAIN = 'localhost'
|
|
|
13
13
|
const SPECIAL_FOLDER = '$'
|
|
14
14
|
const EXTENSIONS_FOLDER_NAME = 'extensions'
|
|
15
15
|
const EXTENSION_EVENT = 'extension-message'
|
|
16
|
+
const VERSION_FOLDER_NAME = 'version'
|
|
16
17
|
const PEER_OPEN = 'peer-open'
|
|
17
18
|
const PEER_REMOVE = 'peer-remove'
|
|
18
19
|
|
|
@@ -54,6 +55,26 @@ export default async function makeHyperFetch ({
|
|
|
54
55
|
const extensions = new Map()
|
|
55
56
|
const cores = new Map()
|
|
56
57
|
|
|
58
|
+
if (extensionMessages) {
|
|
59
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`, listExtensions)
|
|
60
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, listenExtension)
|
|
61
|
+
router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, broadcastExtension)
|
|
62
|
+
router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*/*`, extensionToPeer)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (writable) {
|
|
66
|
+
router.get(`hyper://${SPECIAL_DOMAIN}/`, getKey)
|
|
67
|
+
router.post(`hyper://${SPECIAL_DOMAIN}/`, createKey)
|
|
68
|
+
|
|
69
|
+
router.put('hyper://*/**', putFiles)
|
|
70
|
+
router.delete('hyper://*/**', deleteFiles)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
router.head(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, headFilesVersioned)
|
|
74
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, getFilesVersioned)
|
|
75
|
+
router.get('hyper://*/**', getFiles)
|
|
76
|
+
router.head('hyper://*/**', headFiles)
|
|
77
|
+
|
|
57
78
|
async function getCore (hostname) {
|
|
58
79
|
if (cores.has(hostname)) {
|
|
59
80
|
return cores.get(hostname)
|
|
@@ -155,225 +176,226 @@ export default async function makeHyperFetch ({
|
|
|
155
176
|
return [...core.extensions.keys()]
|
|
156
177
|
}
|
|
157
178
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const accept = request.headers.get('Accept') || ''
|
|
162
|
-
|
|
163
|
-
const core = await getCore(`hyper://${hostname}/`)
|
|
179
|
+
async function listExtensions (request) {
|
|
180
|
+
const { hostname } = new URL(request.url)
|
|
181
|
+
const accept = request.headers.get('Accept') || ''
|
|
164
182
|
|
|
165
|
-
|
|
166
|
-
const events = new EventIterator(({ push }) => {
|
|
167
|
-
function onMessage (name, content, peer) {
|
|
168
|
-
const id = peer.remotePublicKey.toString('hex')
|
|
169
|
-
// TODO: Fancy verification on the `name`?
|
|
170
|
-
// Send each line of content separately on a `data` line
|
|
171
|
-
const data = content.split('\n').map((line) => `data:${line}\n`).join('')
|
|
183
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
172
184
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Whatever, probably an uninitialized peer
|
|
181
|
-
if (!peer.remotePublicKey) return
|
|
182
|
-
const id = peer.remotePublicKey.toString('hex')
|
|
183
|
-
push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
|
|
184
|
-
}
|
|
185
|
-
core.on(EXTENSION_EVENT, onMessage)
|
|
186
|
-
core.on(PEER_OPEN, onPeerOpen)
|
|
187
|
-
core.on(PEER_REMOVE, onPeerRemove)
|
|
188
|
-
return () => {
|
|
189
|
-
core.removeListener(EXTENSION_EVENT, onMessage)
|
|
190
|
-
core.removeListener(PEER_OPEN, onPeerOpen)
|
|
191
|
-
core.removeListener(PEER_REMOVE, onPeerRemove)
|
|
192
|
-
}
|
|
193
|
-
})
|
|
185
|
+
if (accept.includes('text/event-stream')) {
|
|
186
|
+
const events = new EventIterator(({ push }) => {
|
|
187
|
+
function onMessage (name, content, peer) {
|
|
188
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
189
|
+
// TODO: Fancy verification on the `name`?
|
|
190
|
+
// Send each line of content separately on a `data` line
|
|
191
|
+
const data = content.split('\n').map((line) => `data:${line}\n`).join('')
|
|
194
192
|
|
|
195
|
-
|
|
196
|
-
statusCode: 200,
|
|
197
|
-
headers: {
|
|
198
|
-
[HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
|
|
199
|
-
},
|
|
200
|
-
body: events
|
|
193
|
+
push(`id:${id}\nevent:${name}\n${data}\n`)
|
|
201
194
|
}
|
|
202
|
-
|
|
195
|
+
function onPeerOpen (peer) {
|
|
196
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
197
|
+
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
198
|
+
}
|
|
199
|
+
function onPeerRemove (peer) {
|
|
200
|
+
// Whatever, probably an uninitialized peer
|
|
201
|
+
if (!peer.remotePublicKey) return
|
|
202
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
203
|
+
push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
|
|
204
|
+
}
|
|
205
|
+
core.on(EXTENSION_EVENT, onMessage)
|
|
206
|
+
core.on(PEER_OPEN, onPeerOpen)
|
|
207
|
+
core.on(PEER_REMOVE, onPeerRemove)
|
|
208
|
+
return () => {
|
|
209
|
+
core.removeListener(EXTENSION_EVENT, onMessage)
|
|
210
|
+
core.removeListener(PEER_OPEN, onPeerOpen)
|
|
211
|
+
core.removeListener(PEER_REMOVE, onPeerRemove)
|
|
212
|
+
}
|
|
213
|
+
})
|
|
203
214
|
|
|
204
|
-
const extensions = listExtensionNames(core)
|
|
205
215
|
return {
|
|
206
|
-
|
|
207
|
-
headers: {
|
|
208
|
-
|
|
216
|
+
statusCode: 200,
|
|
217
|
+
headers: {
|
|
218
|
+
[HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
|
|
219
|
+
},
|
|
220
|
+
body: events
|
|
209
221
|
}
|
|
210
|
-
}
|
|
211
|
-
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, async function listenExtension (request) {
|
|
212
|
-
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
213
|
-
const pathname = decodeURI(rawPathname)
|
|
214
|
-
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
222
|
+
}
|
|
215
223
|
|
|
216
|
-
|
|
224
|
+
const extensions = listExtensionNames(core)
|
|
225
|
+
return {
|
|
226
|
+
status: 200,
|
|
227
|
+
headers: { [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON },
|
|
228
|
+
body: JSON.stringify(extensions, null, '\t')
|
|
229
|
+
}
|
|
230
|
+
}
|
|
217
231
|
|
|
218
|
-
|
|
232
|
+
async function listenExtension (request) {
|
|
233
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
234
|
+
const pathname = decodeURI(rawPathname)
|
|
235
|
+
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
219
236
|
|
|
220
|
-
|
|
221
|
-
const finalPeers = formatPeers(peers)
|
|
222
|
-
const body = JSON.stringify(finalPeers, null, '\t')
|
|
237
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
223
238
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
await getExtension(core, name)
|
|
240
|
+
|
|
241
|
+
const peers = await getExtensionPeers(core, name)
|
|
242
|
+
const finalPeers = formatPeers(peers)
|
|
243
|
+
const body = JSON.stringify(finalPeers, null, '\t')
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
status: 200,
|
|
247
|
+
body,
|
|
248
|
+
headers: {
|
|
249
|
+
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
|
|
230
250
|
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
234
|
-
const pathname = decodeURI(rawPathname)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
235
253
|
|
|
236
|
-
|
|
254
|
+
async function broadcastExtension (request) {
|
|
255
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
256
|
+
const pathname = decodeURI(rawPathname)
|
|
237
257
|
|
|
238
|
-
|
|
258
|
+
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
239
259
|
|
|
240
|
-
|
|
241
|
-
const data = await request.text()
|
|
242
|
-
extension.broadcast(data)
|
|
260
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
243
261
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
262
|
+
const extension = await getExtension(core, name)
|
|
263
|
+
const data = await request.text()
|
|
264
|
+
extension.broadcast(data)
|
|
265
|
+
|
|
266
|
+
return { status: 200 }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function extensionToPeer (request) {
|
|
270
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
271
|
+
const pathname = decodeURI(rawPathname)
|
|
272
|
+
|
|
273
|
+
const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
274
|
+
const [name, extensionPeer] = subFolder.split('/')
|
|
275
|
+
|
|
276
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
277
|
+
|
|
278
|
+
const extension = await getExtension(core, name)
|
|
279
|
+
const peers = await getExtensionPeers(core, name)
|
|
280
|
+
const peer = peers.find(({ remotePublicKey }) => remotePublicKey.toString('hex') === extensionPeer)
|
|
281
|
+
if (!peer) {
|
|
282
|
+
return {
|
|
283
|
+
status: 404,
|
|
284
|
+
headers: {
|
|
285
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
286
|
+
},
|
|
287
|
+
body: 'Peer Not Found'
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const data = await request.arrayBuffer()
|
|
291
|
+
extension.send(data, peer)
|
|
292
|
+
return { status: 200 }
|
|
293
|
+
}
|
|
249
294
|
|
|
250
|
-
|
|
251
|
-
|
|
295
|
+
async function getKey (request) {
|
|
296
|
+
const key = new URL(request.url).searchParams.get('key')
|
|
297
|
+
if (!key) {
|
|
298
|
+
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
299
|
+
}
|
|
252
300
|
|
|
253
|
-
|
|
301
|
+
try {
|
|
302
|
+
const drive = await getDriveFromKey(key, true)
|
|
254
303
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (!peer) {
|
|
304
|
+
return { body: drive.url }
|
|
305
|
+
} catch (e) {
|
|
306
|
+
if (e.message === ERROR_KEY_NOT_CREATED) {
|
|
259
307
|
return {
|
|
260
|
-
status:
|
|
308
|
+
status: 400,
|
|
309
|
+
body: e.message,
|
|
261
310
|
headers: {
|
|
262
311
|
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
263
|
-
}
|
|
264
|
-
body: 'Peer Not Found'
|
|
312
|
+
}
|
|
265
313
|
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
extension.send(data, peer)
|
|
269
|
-
return { status: 200 }
|
|
270
|
-
})
|
|
314
|
+
} else throw e
|
|
315
|
+
}
|
|
271
316
|
}
|
|
272
317
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
278
|
-
}
|
|
318
|
+
async function createKey (request) {
|
|
319
|
+
// TODO: Allow importing secret keys here
|
|
320
|
+
// Maybe specify a seed to use for generating the blobs?
|
|
321
|
+
// Else we'd need to specify the blobs keys and metadata keys
|
|
279
322
|
|
|
280
|
-
|
|
281
|
-
|
|
323
|
+
const key = new URL(request.url).searchParams.get('key')
|
|
324
|
+
if (!key) {
|
|
325
|
+
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
326
|
+
}
|
|
282
327
|
|
|
283
|
-
|
|
284
|
-
} catch (e) {
|
|
285
|
-
if (e.message === ERROR_KEY_NOT_CREATED) {
|
|
286
|
-
return {
|
|
287
|
-
status: 400,
|
|
288
|
-
body: e.message,
|
|
289
|
-
headers: {
|
|
290
|
-
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
} else throw e
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
router.post(`hyper://${SPECIAL_DOMAIN}/`, async function createKey (request) {
|
|
297
|
-
// TODO: Allow importing secret keys here
|
|
298
|
-
// Maybe specify a seed to use for generating the blobs?
|
|
299
|
-
// Else we'd need to specify the blobs keys and metadata keys
|
|
300
|
-
|
|
301
|
-
const key = new URL(request.url).searchParams.get('key')
|
|
302
|
-
if (!key) {
|
|
303
|
-
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
304
|
-
}
|
|
328
|
+
const drive = await getDriveFromKey(key, false)
|
|
305
329
|
|
|
306
|
-
|
|
330
|
+
return { body: drive.url }
|
|
331
|
+
}
|
|
307
332
|
|
|
308
|
-
|
|
309
|
-
})
|
|
333
|
+
async function putFiles (request) {
|
|
334
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
335
|
+
const pathname = decodeURI(rawPathname)
|
|
336
|
+
const contentType = request.headers.get('Content-Type') || ''
|
|
337
|
+
const isFormData = contentType.includes('multipart/form-data')
|
|
310
338
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (isFormData) {
|
|
320
|
-
// It's a form! Get the files out and process them
|
|
321
|
-
const formData = await request.formData()
|
|
322
|
-
for (const [name, data] of formData) {
|
|
323
|
-
if (name !== 'file') continue
|
|
324
|
-
const filePath = posix.join(pathname, data.name)
|
|
325
|
-
await pipelinePromise(
|
|
326
|
-
Readable.from(data.stream()),
|
|
327
|
-
drive.createWriteStream(filePath, {
|
|
328
|
-
metadata: {
|
|
329
|
-
mtime: Date.now()
|
|
330
|
-
}
|
|
331
|
-
})
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
339
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
340
|
+
|
|
341
|
+
if (isFormData) {
|
|
342
|
+
// It's a form! Get the files out and process them
|
|
343
|
+
const formData = await request.formData()
|
|
344
|
+
for (const [name, data] of formData) {
|
|
345
|
+
if (name !== 'file') continue
|
|
346
|
+
const filePath = posix.join(pathname, data.name)
|
|
335
347
|
await pipelinePromise(
|
|
336
|
-
Readable.from(
|
|
337
|
-
drive.createWriteStream(
|
|
348
|
+
Readable.from(data.stream()),
|
|
349
|
+
drive.createWriteStream(filePath, {
|
|
338
350
|
metadata: {
|
|
339
351
|
mtime: Date.now()
|
|
340
352
|
}
|
|
341
353
|
})
|
|
342
354
|
)
|
|
343
355
|
}
|
|
356
|
+
} else {
|
|
357
|
+
await pipelinePromise(
|
|
358
|
+
Readable.from(request.body),
|
|
359
|
+
drive.createWriteStream(pathname, {
|
|
360
|
+
metadata: {
|
|
361
|
+
mtime: Date.now()
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
)
|
|
365
|
+
}
|
|
344
366
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
router.delete('hyper://*/**', async function putFiles (request) {
|
|
348
|
-
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
349
|
-
const pathname = decodeURI(rawPathname)
|
|
350
|
-
|
|
351
|
-
const drive = await getDrive(`hyper://${hostname}`)
|
|
367
|
+
return { status: 201, headers: { Location: request.url } }
|
|
368
|
+
}
|
|
352
369
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
await drive.del(entry.key)
|
|
357
|
-
didDelete = true
|
|
358
|
-
}
|
|
359
|
-
if (!didDelete) {
|
|
360
|
-
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
361
|
-
}
|
|
362
|
-
return { status: 200 }
|
|
363
|
-
}
|
|
370
|
+
async function deleteFiles (request) {
|
|
371
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
372
|
+
const pathname = decodeURI(rawPathname)
|
|
364
373
|
|
|
365
|
-
|
|
374
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
366
375
|
|
|
367
|
-
|
|
376
|
+
if (pathname.endsWith('/')) {
|
|
377
|
+
let didDelete = false
|
|
378
|
+
for await (const entry of drive.list(pathname)) {
|
|
379
|
+
await drive.del(entry.key)
|
|
380
|
+
didDelete = true
|
|
381
|
+
}
|
|
382
|
+
if (!didDelete) {
|
|
368
383
|
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
369
384
|
}
|
|
370
|
-
await drive.del(pathname)
|
|
371
|
-
|
|
372
385
|
return { status: 200 }
|
|
373
|
-
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const entry = await drive.entry(pathname)
|
|
389
|
+
|
|
390
|
+
if (!entry) {
|
|
391
|
+
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
392
|
+
}
|
|
393
|
+
await drive.del(pathname)
|
|
394
|
+
|
|
395
|
+
return { status: 200 }
|
|
374
396
|
}
|
|
375
397
|
|
|
376
|
-
|
|
398
|
+
async function headFilesVersioned (request) {
|
|
377
399
|
const url = new URL(request.url)
|
|
378
400
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
379
401
|
const pathname = decodeURI(rawPathname)
|
|
@@ -381,10 +403,35 @@ export default async function makeHyperFetch ({
|
|
|
381
403
|
const accept = request.headers.get('Accept') || ''
|
|
382
404
|
const isRanged = request.headers.get('Range') || ''
|
|
383
405
|
const noResolve = searchParams.has('noResolve')
|
|
384
|
-
|
|
406
|
+
|
|
407
|
+
const parts = pathname.split('/')
|
|
408
|
+
const version = parts[3]
|
|
409
|
+
const realPath = parts.slice(4).join('/')
|
|
385
410
|
|
|
386
411
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
387
|
-
|
|
412
|
+
|
|
413
|
+
const snapshot = await drive.checkout(version)
|
|
414
|
+
|
|
415
|
+
return serveHead(snapshot, realPath, { accept, isRanged, noResolve })
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function headFiles (request) {
|
|
419
|
+
const url = new URL(request.url)
|
|
420
|
+
const { hostname, pathname: rawPathname, searchParams } = url
|
|
421
|
+
const pathname = decodeURI(rawPathname)
|
|
422
|
+
|
|
423
|
+
const accept = request.headers.get('Accept') || ''
|
|
424
|
+
const isRanged = request.headers.get('Range') || ''
|
|
425
|
+
const noResolve = searchParams.has('noResolve')
|
|
426
|
+
|
|
427
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
428
|
+
|
|
429
|
+
return serveHead(drive, pathname, { accept, isRanged, noResolve })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function serveHead (drive, pathname, { accept, isRanged, noResolve }) {
|
|
433
|
+
const isDirectory = pathname.endsWith('/')
|
|
434
|
+
const fullURL = new URL(pathname, drive.url).href
|
|
388
435
|
|
|
389
436
|
const resHeaders = {
|
|
390
437
|
ETag: `${drive.version}`,
|
|
@@ -478,20 +525,46 @@ export default async function makeHyperFetch ({
|
|
|
478
525
|
status: 200,
|
|
479
526
|
headers: resHeaders
|
|
480
527
|
}
|
|
481
|
-
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function getFilesVersioned (request) {
|
|
531
|
+
const url = new URL(request.url)
|
|
532
|
+
const { hostname, pathname: rawPathname, searchParams } = url
|
|
533
|
+
const pathname = decodeURI(rawPathname)
|
|
534
|
+
|
|
535
|
+
const accept = request.headers.get('Accept') || ''
|
|
536
|
+
const isRanged = request.headers.get('Range') || ''
|
|
537
|
+
const noResolve = searchParams.has('noResolve')
|
|
538
|
+
|
|
539
|
+
const parts = pathname.split('/')
|
|
540
|
+
const version = parts[3]
|
|
541
|
+
const realPath = parts.slice(4).join('/')
|
|
542
|
+
|
|
543
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
544
|
+
|
|
545
|
+
const snapshot = await drive.checkout(version)
|
|
546
|
+
|
|
547
|
+
return serveGet(snapshot, realPath, { accept, isRanged, noResolve })
|
|
548
|
+
}
|
|
482
549
|
|
|
483
550
|
// TODO: Redirect on directories without trailing slash
|
|
484
|
-
|
|
551
|
+
async function getFiles (request) {
|
|
485
552
|
const url = new URL(request.url)
|
|
486
553
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
487
554
|
const pathname = decodeURI(rawPathname)
|
|
488
555
|
|
|
489
556
|
const accept = request.headers.get('Accept') || ''
|
|
557
|
+
const isRanged = request.headers.get('Range') || ''
|
|
490
558
|
const noResolve = searchParams.has('noResolve')
|
|
491
|
-
const isDirectory = pathname.endsWith('/')
|
|
492
559
|
|
|
493
560
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
494
|
-
|
|
561
|
+
|
|
562
|
+
return serveGet(drive, pathname, { accept, isRanged, noResolve })
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function serveGet (drive, pathname, { accept, isRanged, noResolve }) {
|
|
566
|
+
const isDirectory = pathname.endsWith('/')
|
|
567
|
+
const fullURL = new URL(pathname, drive.url).href
|
|
495
568
|
|
|
496
569
|
if (isDirectory) {
|
|
497
570
|
const resHeaders = {
|
|
@@ -514,12 +587,12 @@ export default async function makeHyperFetch ({
|
|
|
514
587
|
|
|
515
588
|
if (!noResolve) {
|
|
516
589
|
if (entries.includes('index.html')) {
|
|
517
|
-
return serveFile(
|
|
590
|
+
return serveFile(drive, posix.join(pathname, 'index.html'), isRanged)
|
|
518
591
|
}
|
|
519
592
|
}
|
|
520
593
|
|
|
521
594
|
if (accept.includes('text/html')) {
|
|
522
|
-
const body = await renderIndex(
|
|
595
|
+
const body = await renderIndex(new URL(fullURL), entries, fetch)
|
|
523
596
|
return {
|
|
524
597
|
status: 200,
|
|
525
598
|
body,
|
|
@@ -546,17 +619,16 @@ export default async function makeHyperFetch ({
|
|
|
546
619
|
return { status: 404, body: 'Not Found' }
|
|
547
620
|
}
|
|
548
621
|
|
|
549
|
-
return serveFile(
|
|
550
|
-
}
|
|
622
|
+
return serveFile(drive, path, isRanged)
|
|
623
|
+
}
|
|
551
624
|
|
|
552
625
|
return fetch
|
|
553
626
|
}
|
|
554
627
|
|
|
555
|
-
async function serveFile (
|
|
556
|
-
const isRanged = headers.get('Range') || ''
|
|
628
|
+
async function serveFile (drive, pathname, isRanged) {
|
|
557
629
|
const contentType = getMimeType(pathname)
|
|
558
630
|
|
|
559
|
-
const fullURL = new URL(pathname, drive.
|
|
631
|
+
const fullURL = new URL(pathname, drive.url).href
|
|
560
632
|
|
|
561
633
|
const entry = await drive.entry(pathname)
|
|
562
634
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore-fetch",
|
|
3
|
-
"version": "9.0
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"description": "Implementation of Fetch that uses the Dat SDK for loading p2p content",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@rangermauve/fetch-event-source": "^1.0.3",
|
|
35
|
-
"hyper-sdk": "^4.
|
|
35
|
+
"hyper-sdk": "^4.2.3",
|
|
36
36
|
"standard": "^17.0.0",
|
|
37
37
|
"tape": "^5.2.2"
|
|
38
38
|
}
|
package/test.js
CHANGED
|
@@ -446,11 +446,58 @@ test('EventSource extension messages', async (t) => {
|
|
|
446
446
|
t.ok(lastEventId, 'Event contained peer ID')
|
|
447
447
|
})
|
|
448
448
|
|
|
449
|
+
test('Resolve DNS', async (t) => {
|
|
450
|
+
const loadResponse = await fetch('hyper://example2.mauve.moe/?noResolve')
|
|
451
|
+
|
|
452
|
+
const entries = await loadResponse.json()
|
|
453
|
+
|
|
454
|
+
t.ok(entries.length, 'Loaded contents with some files present')
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
test('Error on invalid hostname', async (t) => {
|
|
458
|
+
const loadResponse = await fetch('hyper://example/')
|
|
459
|
+
|
|
460
|
+
if (loadResponse.ok) {
|
|
461
|
+
throw new Error('Loading without DNS or a public key should have failed')
|
|
462
|
+
} else {
|
|
463
|
+
t.pass('Invalid names led to an error')
|
|
464
|
+
}
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
test('GET older version of file from VERSION folder', async (t) => {
|
|
468
|
+
const created = await nextURL(t)
|
|
469
|
+
|
|
470
|
+
const fileName = 'example.txt'
|
|
471
|
+
|
|
472
|
+
const data1 = 'Hello World'
|
|
473
|
+
const data2 = 'Goodbye World'
|
|
474
|
+
|
|
475
|
+
const fileURL = new URL(`/${fileName}`, created)
|
|
476
|
+
const versionFileURL = new URL(`/$/version/2/${fileName}`, created)
|
|
477
|
+
|
|
478
|
+
await checkResponse(
|
|
479
|
+
await fetch(fileURL, { method: 'PUT', body: data1 }), t
|
|
480
|
+
)
|
|
481
|
+
await checkResponse(
|
|
482
|
+
await fetch(fileURL, { method: 'PUT', body: data2 }), t
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
const versionedResponse = await fetch(versionFileURL)
|
|
486
|
+
|
|
487
|
+
await checkResponse(versionedResponse, t, 'Able to GET versioned file')
|
|
488
|
+
|
|
489
|
+
const versionedData = await versionedResponse.text()
|
|
490
|
+
|
|
491
|
+
t.equal(versionedData, data1, 'Old data got loaded')
|
|
492
|
+
})
|
|
493
|
+
|
|
449
494
|
async function checkResponse (response, t, successMessage = 'Response OK') {
|
|
450
495
|
if (!response.ok) {
|
|
451
496
|
const message = await response.text()
|
|
452
497
|
t.fail(new Error(`HTTP Error ${response.status}:\n${message}`))
|
|
498
|
+
return false
|
|
453
499
|
} else {
|
|
454
500
|
t.pass(successMessage)
|
|
501
|
+
return true
|
|
455
502
|
}
|
|
456
503
|
}
|