hypercore-fetch 9.0.8 → 9.2.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.
Files changed (4) hide show
  1. package/README.md +12 -0
  2. package/index.js +260 -183
  3. package/package.json +1 -1
  4. package/test.js +33 -6
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
 
@@ -39,6 +40,11 @@ async function DEFAULT_RENDER_INDEX (url, files, fetch) {
39
40
  `
40
41
  }
41
42
 
43
+ // Support gemini files
44
+ mime.define({
45
+ 'text/gemini': ['gmi', 'gemini']
46
+ }, true)
47
+
42
48
  export default async function makeHyperFetch ({
43
49
  sdk,
44
50
  writable = false,
@@ -54,6 +60,26 @@ export default async function makeHyperFetch ({
54
60
  const extensions = new Map()
55
61
  const cores = new Map()
56
62
 
63
+ if (extensionMessages) {
64
+ router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`, listExtensions)
65
+ router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, listenExtension)
66
+ router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, broadcastExtension)
67
+ router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*/*`, extensionToPeer)
68
+ }
69
+
70
+ if (writable) {
71
+ router.get(`hyper://${SPECIAL_DOMAIN}/`, getKey)
72
+ router.post(`hyper://${SPECIAL_DOMAIN}/`, createKey)
73
+
74
+ router.put('hyper://*/**', putFiles)
75
+ router.delete('hyper://*/**', deleteFiles)
76
+ }
77
+
78
+ router.head(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, headFilesVersioned)
79
+ router.get(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, getFilesVersioned)
80
+ router.get('hyper://*/**', getFiles)
81
+ router.head('hyper://*/**', headFiles)
82
+
57
83
  async function getCore (hostname) {
58
84
  if (cores.has(hostname)) {
59
85
  return cores.get(hostname)
@@ -155,225 +181,246 @@ export default async function makeHyperFetch ({
155
181
  return [...core.extensions.keys()]
156
182
  }
157
183
 
158
- if (extensionMessages) {
159
- router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`, async function listExtensions (request) {
160
- const { hostname } = new URL(request.url)
161
- const accept = request.headers.get('Accept') || ''
184
+ async function listExtensions (request) {
185
+ const { hostname } = new URL(request.url)
186
+ const accept = request.headers.get('Accept') || ''
162
187
 
163
- const core = await getCore(`hyper://${hostname}/`)
188
+ const core = await getCore(`hyper://${hostname}/`)
164
189
 
165
- if (accept.includes('text/event-stream')) {
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('')
190
+ if (accept.includes('text/event-stream')) {
191
+ const events = new EventIterator(({ push }) => {
192
+ function onMessage (name, content, peer) {
193
+ const id = peer.remotePublicKey.toString('hex')
194
+ // TODO: Fancy verification on the `name`?
195
+ // Send each line of content separately on a `data` line
196
+ const data = content.split('\n').map((line) => `data:${line}\n`).join('')
172
197
 
173
- push(`id:${id}\nevent:${name}\n${data}\n`)
174
- }
175
- function onPeerOpen (peer) {
176
- const id = peer.remotePublicKey.toString('hex')
177
- push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
178
- }
179
- function onPeerRemove (peer) {
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
- })
194
-
195
- return {
196
- statusCode: 200,
197
- headers: {
198
- [HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
199
- },
200
- body: events
198
+ push(`id:${id}\nevent:${name}\n${data}\n`)
201
199
  }
202
- }
200
+ function onPeerOpen (peer) {
201
+ const id = peer.remotePublicKey.toString('hex')
202
+ push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
203
+ }
204
+ function onPeerRemove (peer) {
205
+ // Whatever, probably an uninitialized peer
206
+ if (!peer.remotePublicKey) return
207
+ const id = peer.remotePublicKey.toString('hex')
208
+ push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
209
+ }
210
+ core.on(EXTENSION_EVENT, onMessage)
211
+ core.on(PEER_OPEN, onPeerOpen)
212
+ core.on(PEER_REMOVE, onPeerRemove)
213
+ return () => {
214
+ core.removeListener(EXTENSION_EVENT, onMessage)
215
+ core.removeListener(PEER_OPEN, onPeerOpen)
216
+ core.removeListener(PEER_REMOVE, onPeerRemove)
217
+ }
218
+ })
203
219
 
204
- const extensions = listExtensionNames(core)
205
220
  return {
206
- status: 200,
207
- headers: { [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON },
208
- body: JSON.stringify(extensions, null, '\t')
221
+ statusCode: 200,
222
+ headers: {
223
+ [HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
224
+ },
225
+ body: events
209
226
  }
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)
227
+ }
215
228
 
216
- const core = await getCore(`hyper://${hostname}/`)
229
+ const extensions = listExtensionNames(core)
230
+ return {
231
+ status: 200,
232
+ headers: { [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON },
233
+ body: JSON.stringify(extensions, null, '\t')
234
+ }
235
+ }
217
236
 
218
- await getExtension(core, name)
237
+ async function listenExtension (request) {
238
+ const { hostname, pathname: rawPathname } = new URL(request.url)
239
+ const pathname = decodeURI(rawPathname)
240
+ const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
219
241
 
220
- const peers = await getExtensionPeers(core, name)
221
- const finalPeers = formatPeers(peers)
222
- const body = JSON.stringify(finalPeers, null, '\t')
242
+ const core = await getCore(`hyper://${hostname}/`)
223
243
 
224
- return {
225
- status: 200,
226
- body,
227
- headers: {
228
- [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
229
- }
244
+ await getExtension(core, name)
245
+
246
+ const peers = await getExtensionPeers(core, name)
247
+ const finalPeers = formatPeers(peers)
248
+ const body = JSON.stringify(finalPeers, null, '\t')
249
+
250
+ return {
251
+ status: 200,
252
+ body,
253
+ headers: {
254
+ [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
230
255
  }
231
- })
232
- router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, async function broadcastExtension (request) {
233
- const { hostname, pathname: rawPathname } = new URL(request.url)
234
- const pathname = decodeURI(rawPathname)
256
+ }
257
+ }
235
258
 
236
- const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
259
+ async function broadcastExtension (request) {
260
+ const { hostname, pathname: rawPathname } = new URL(request.url)
261
+ const pathname = decodeURI(rawPathname)
237
262
 
238
- const core = await getCore(`hyper://${hostname}/`)
263
+ const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
239
264
 
240
- const extension = await getExtension(core, name)
241
- const data = await request.text()
242
- extension.broadcast(data)
265
+ const core = await getCore(`hyper://${hostname}/`)
243
266
 
244
- return { status: 200 }
245
- })
246
- router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*/*`, async function extensionToPeer (request) {
247
- const { hostname, pathname: rawPathname } = new URL(request.url)
248
- const pathname = decodeURI(rawPathname)
267
+ const extension = await getExtension(core, name)
268
+ const data = await request.text()
269
+ extension.broadcast(data)
249
270
 
250
- const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
251
- const [name, extensionPeer] = subFolder.split('/')
271
+ return { status: 200 }
272
+ }
273
+
274
+ async function extensionToPeer (request) {
275
+ const { hostname, pathname: rawPathname } = new URL(request.url)
276
+ const pathname = decodeURI(rawPathname)
252
277
 
253
- const core = await getCore(`hyper://${hostname}/`)
278
+ const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
279
+ const [name, extensionPeer] = subFolder.split('/')
254
280
 
255
- const extension = await getExtension(core, name)
256
- const peers = await getExtensionPeers(core, name)
257
- const peer = peers.find(({ remotePublicKey }) => remotePublicKey.toString('hex') === extensionPeer)
258
- if (!peer) {
281
+ const core = await getCore(`hyper://${hostname}/`)
282
+
283
+ const extension = await getExtension(core, name)
284
+ const peers = await getExtensionPeers(core, name)
285
+ const peer = peers.find(({ remotePublicKey }) => remotePublicKey.toString('hex') === extensionPeer)
286
+ if (!peer) {
287
+ return {
288
+ status: 404,
289
+ headers: {
290
+ [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
291
+ },
292
+ body: 'Peer Not Found'
293
+ }
294
+ }
295
+ const data = await request.arrayBuffer()
296
+ extension.send(data, peer)
297
+ return { status: 200 }
298
+ }
299
+
300
+ async function getKey (request) {
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
+ }
305
+
306
+ try {
307
+ const drive = await getDriveFromKey(key, true)
308
+
309
+ return { body: drive.url }
310
+ } catch (e) {
311
+ if (e.message === ERROR_KEY_NOT_CREATED) {
259
312
  return {
260
- status: 404,
313
+ status: 400,
314
+ body: e.message,
261
315
  headers: {
262
316
  [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
263
- },
264
- body: 'Peer Not Found'
317
+ }
265
318
  }
266
- }
267
- const data = await request.arrayBuffer()
268
- extension.send(data, peer)
269
- return { status: 200 }
270
- })
319
+ } else throw e
320
+ }
271
321
  }
272
322
 
273
- if (writable) {
274
- router.get(`hyper://${SPECIAL_DOMAIN}/`, async function getKey (request) {
275
- const key = new URL(request.url).searchParams.get('key')
276
- if (!key) {
277
- return { status: 400, body: 'Must specify key parameter to resolve' }
278
- }
323
+ async function createKey (request) {
324
+ // TODO: Allow importing secret keys here
325
+ // Maybe specify a seed to use for generating the blobs?
326
+ // Else we'd need to specify the blobs keys and metadata keys
279
327
 
280
- try {
281
- const drive = await getDriveFromKey(key, true)
328
+ const key = new URL(request.url).searchParams.get('key')
329
+ if (!key) {
330
+ return { status: 400, body: 'Must specify key parameter to resolve' }
331
+ }
282
332
 
283
- return { body: drive.url }
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
- }
333
+ const drive = await getDriveFromKey(key, false)
305
334
 
306
- const drive = await getDriveFromKey(key, false)
335
+ return { body: drive.url }
336
+ }
307
337
 
308
- return { body: drive.url }
309
- })
338
+ async function putFiles (request) {
339
+ const { hostname, pathname: rawPathname } = new URL(request.url)
340
+ const pathname = decodeURI(rawPathname)
341
+ const contentType = request.headers.get('Content-Type') || ''
342
+ const isFormData = contentType.includes('multipart/form-data')
310
343
 
311
- router.put('hyper://*/**', async function putFiles (request) {
312
- const { hostname, pathname: rawPathname } = new URL(request.url)
313
- const pathname = decodeURI(rawPathname)
314
- const contentType = request.headers.get('Content-Type') || ''
315
- const isFormData = contentType.includes('multipart/form-data')
316
-
317
- const drive = await getDrive(`hyper://${hostname}`)
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 {
344
+ const drive = await getDrive(`hyper://${hostname}`)
345
+
346
+ if (isFormData) {
347
+ // It's a form! Get the files out and process them
348
+ const formData = await request.formData()
349
+ for (const [name, data] of formData) {
350
+ if (name !== 'file') continue
351
+ const filePath = posix.join(pathname, data.name)
335
352
  await pipelinePromise(
336
- Readable.from(request.body),
337
- drive.createWriteStream(pathname, {
353
+ Readable.from(data.stream()),
354
+ drive.createWriteStream(filePath, {
338
355
  metadata: {
339
356
  mtime: Date.now()
340
357
  }
341
358
  })
342
359
  )
343
360
  }
361
+ } else {
362
+ await pipelinePromise(
363
+ Readable.from(request.body),
364
+ drive.createWriteStream(pathname, {
365
+ metadata: {
366
+ mtime: Date.now()
367
+ }
368
+ })
369
+ )
370
+ }
344
371
 
345
- return { status: 201, headers: { Location: request.url } }
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}`)
372
+ return { status: 201, headers: { Location: request.url } }
373
+ }
352
374
 
353
- if (pathname.endsWith('/')) {
354
- let didDelete = false
355
- for await (const entry of drive.list(pathname)) {
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
- }
375
+ async function deleteFiles (request) {
376
+ const { hostname, pathname: rawPathname } = new URL(request.url)
377
+ const pathname = decodeURI(rawPathname)
364
378
 
365
- const entry = await drive.entry(pathname)
379
+ const drive = await getDrive(`hyper://${hostname}`)
366
380
 
367
- if (!entry) {
381
+ if (pathname.endsWith('/')) {
382
+ let didDelete = false
383
+ for await (const entry of drive.list(pathname)) {
384
+ await drive.del(entry.key)
385
+ didDelete = true
386
+ }
387
+ if (!didDelete) {
368
388
  return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
369
389
  }
370
- await drive.del(pathname)
371
-
372
390
  return { status: 200 }
373
- })
391
+ }
392
+
393
+ const entry = await drive.entry(pathname)
394
+
395
+ if (!entry) {
396
+ return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
397
+ }
398
+ await drive.del(pathname)
399
+
400
+ return { status: 200 }
374
401
  }
375
402
 
376
- router.head('hyper://*/**', async function headFiles (request) {
403
+ async function headFilesVersioned (request) {
404
+ const url = new URL(request.url)
405
+ const { hostname, pathname: rawPathname, searchParams } = url
406
+ const pathname = decodeURI(rawPathname)
407
+
408
+ const accept = request.headers.get('Accept') || ''
409
+ const isRanged = request.headers.get('Range') || ''
410
+ const noResolve = searchParams.has('noResolve')
411
+
412
+ const parts = pathname.split('/')
413
+ const version = parts[3]
414
+ const realPath = parts.slice(4).join('/')
415
+
416
+ const drive = await getDrive(`hyper://${hostname}`)
417
+
418
+ const snapshot = await drive.checkout(version)
419
+
420
+ return serveHead(snapshot, realPath, { accept, isRanged, noResolve })
421
+ }
422
+
423
+ async function headFiles (request) {
377
424
  const url = new URL(request.url)
378
425
  const { hostname, pathname: rawPathname, searchParams } = url
379
426
  const pathname = decodeURI(rawPathname)
@@ -381,9 +428,14 @@ export default async function makeHyperFetch ({
381
428
  const accept = request.headers.get('Accept') || ''
382
429
  const isRanged = request.headers.get('Range') || ''
383
430
  const noResolve = searchParams.has('noResolve')
384
- const isDirectory = pathname.endsWith('/')
385
431
 
386
432
  const drive = await getDrive(`hyper://${hostname}`)
433
+
434
+ return serveHead(drive, pathname, { accept, isRanged, noResolve })
435
+ }
436
+
437
+ async function serveHead (drive, pathname, { accept, isRanged, noResolve }) {
438
+ const isDirectory = pathname.endsWith('/')
387
439
  const fullURL = new URL(pathname, drive.url).href
388
440
 
389
441
  const resHeaders = {
@@ -478,19 +530,45 @@ export default async function makeHyperFetch ({
478
530
  status: 200,
479
531
  headers: resHeaders
480
532
  }
481
- })
533
+ }
534
+
535
+ async function getFilesVersioned (request) {
536
+ const url = new URL(request.url)
537
+ const { hostname, pathname: rawPathname, searchParams } = url
538
+ const pathname = decodeURI(rawPathname)
539
+
540
+ const accept = request.headers.get('Accept') || ''
541
+ const isRanged = request.headers.get('Range') || ''
542
+ const noResolve = searchParams.has('noResolve')
543
+
544
+ const parts = pathname.split('/')
545
+ const version = parts[3]
546
+ const realPath = parts.slice(4).join('/')
547
+
548
+ const drive = await getDrive(`hyper://${hostname}`)
549
+
550
+ const snapshot = await drive.checkout(version)
551
+
552
+ return serveGet(snapshot, realPath, { accept, isRanged, noResolve })
553
+ }
482
554
 
483
555
  // TODO: Redirect on directories without trailing slash
484
- router.get('hyper://*/**', async function getFiles (request) {
556
+ async function getFiles (request) {
485
557
  const url = new URL(request.url)
486
558
  const { hostname, pathname: rawPathname, searchParams } = url
487
559
  const pathname = decodeURI(rawPathname)
488
560
 
489
561
  const accept = request.headers.get('Accept') || ''
562
+ const isRanged = request.headers.get('Range') || ''
490
563
  const noResolve = searchParams.has('noResolve')
491
- const isDirectory = pathname.endsWith('/')
492
564
 
493
565
  const drive = await getDrive(`hyper://${hostname}`)
566
+
567
+ return serveGet(drive, pathname, { accept, isRanged, noResolve })
568
+ }
569
+
570
+ async function serveGet (drive, pathname, { accept, isRanged, noResolve }) {
571
+ const isDirectory = pathname.endsWith('/')
494
572
  const fullURL = new URL(pathname, drive.url).href
495
573
 
496
574
  if (isDirectory) {
@@ -514,12 +592,12 @@ export default async function makeHyperFetch ({
514
592
 
515
593
  if (!noResolve) {
516
594
  if (entries.includes('index.html')) {
517
- return serveFile(request.headers, drive, posix.join(pathname, 'index.html'))
595
+ return serveFile(drive, posix.join(pathname, 'index.html'), isRanged)
518
596
  }
519
597
  }
520
598
 
521
599
  if (accept.includes('text/html')) {
522
- const body = await renderIndex(url, entries, fetch)
600
+ const body = await renderIndex(new URL(fullURL), entries, fetch)
523
601
  return {
524
602
  status: 200,
525
603
  body,
@@ -546,14 +624,13 @@ export default async function makeHyperFetch ({
546
624
  return { status: 404, body: 'Not Found' }
547
625
  }
548
626
 
549
- return serveFile(request.headers, drive, path)
550
- })
627
+ return serveFile(drive, path, isRanged)
628
+ }
551
629
 
552
630
  return fetch
553
631
  }
554
632
 
555
- async function serveFile (headers, drive, pathname) {
556
- const isRanged = headers.get('Range') || ''
633
+ async function serveFile (drive, pathname, isRanged) {
557
634
  const contentType = getMimeType(pathname)
558
635
 
559
636
  const fullURL = new URL(pathname, drive.url).href
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypercore-fetch",
3
- "version": "9.0.8",
3
+ "version": "9.2.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",
package/test.js CHANGED
@@ -447,30 +447,57 @@ test('EventSource extension messages', async (t) => {
447
447
  })
448
448
 
449
449
  test('Resolve DNS', async (t) => {
450
- const loadResponse = await fetch('hyper://example2.mauve.moe/')
450
+ const loadResponse = await fetch('hyper://example2.mauve.moe/?noResolve')
451
451
 
452
452
  const entries = await loadResponse.json()
453
453
 
454
- t.pass('Loaded contents and ')
454
+ t.ok(entries.length, 'Loaded contents with some files present')
455
455
  })
456
456
 
457
457
  test('Error on invalid hostname', async (t) => {
458
458
  const loadResponse = await fetch('hyper://example/')
459
459
 
460
- console.log(loadResponse.status)
461
-
462
- if(loadResponse.ok) {
460
+ if (loadResponse.ok) {
463
461
  throw new Error('Loading without DNS or a public key should have failed')
464
462
  } else {
465
- t.pass("Invalid names led to an error")
463
+ t.pass('Invalid names led to an error')
466
464
  }
467
465
  })
468
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
+
469
494
  async function checkResponse (response, t, successMessage = 'Response OK') {
470
495
  if (!response.ok) {
471
496
  const message = await response.text()
472
497
  t.fail(new Error(`HTTP Error ${response.status}:\n${message}`))
498
+ return false
473
499
  } else {
474
500
  t.pass(successMessage)
501
+ return true
475
502
  }
476
503
  }