hypercore-fetch 9.0.8 → 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.
Files changed (4) hide show
  1. package/README.md +12 -0
  2. package/index.js +255 -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
 
@@ -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,246 @@ export default async function makeHyperFetch ({
155
176
  return [...core.extensions.keys()]
156
177
  }
157
178
 
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') || ''
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
- 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('')
183
+ const core = await getCore(`hyper://${hostname}/`)
172
184
 
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
- })
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
- return {
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
- status: 200,
207
- headers: { [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON },
208
- body: JSON.stringify(extensions, null, '\t')
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
- const core = await getCore(`hyper://${hostname}/`)
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
- await getExtension(core, name)
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
- const peers = await getExtensionPeers(core, name)
221
- const finalPeers = formatPeers(peers)
222
- const body = JSON.stringify(finalPeers, null, '\t')
237
+ const core = await getCore(`hyper://${hostname}/`)
223
238
 
224
- return {
225
- status: 200,
226
- body,
227
- headers: {
228
- [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
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
- 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)
251
+ }
252
+ }
235
253
 
236
- const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
254
+ async function broadcastExtension (request) {
255
+ const { hostname, pathname: rawPathname } = new URL(request.url)
256
+ const pathname = decodeURI(rawPathname)
237
257
 
238
- const core = await getCore(`hyper://${hostname}/`)
258
+ const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
239
259
 
240
- const extension = await getExtension(core, name)
241
- const data = await request.text()
242
- extension.broadcast(data)
260
+ const core = await getCore(`hyper://${hostname}/`)
243
261
 
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)
262
+ const extension = await getExtension(core, name)
263
+ const data = await request.text()
264
+ extension.broadcast(data)
249
265
 
250
- const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
251
- const [name, extensionPeer] = subFolder.split('/')
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)
252
272
 
253
- const core = await getCore(`hyper://${hostname}/`)
273
+ const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
274
+ const [name, extensionPeer] = subFolder.split('/')
254
275
 
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) {
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
+ }
294
+
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
+ }
300
+
301
+ try {
302
+ const drive = await getDriveFromKey(key, true)
303
+
304
+ return { body: drive.url }
305
+ } catch (e) {
306
+ if (e.message === ERROR_KEY_NOT_CREATED) {
259
307
  return {
260
- status: 404,
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
- const data = await request.arrayBuffer()
268
- extension.send(data, peer)
269
- return { status: 200 }
270
- })
314
+ } else throw e
315
+ }
271
316
  }
272
317
 
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
- }
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
- try {
281
- const drive = await getDriveFromKey(key, true)
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
- 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
- }
328
+ const drive = await getDriveFromKey(key, false)
305
329
 
306
- const drive = await getDriveFromKey(key, false)
330
+ return { body: drive.url }
331
+ }
307
332
 
308
- return { body: drive.url }
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
- 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 {
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(request.body),
337
- drive.createWriteStream(pathname, {
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
- 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}`)
367
+ return { status: 201, headers: { Location: request.url } }
368
+ }
352
369
 
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
- }
370
+ async function deleteFiles (request) {
371
+ const { hostname, pathname: rawPathname } = new URL(request.url)
372
+ const pathname = decodeURI(rawPathname)
364
373
 
365
- const entry = await drive.entry(pathname)
374
+ const drive = await getDrive(`hyper://${hostname}`)
366
375
 
367
- if (!entry) {
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
- router.head('hyper://*/**', async function headFiles (request) {
398
+ async function headFilesVersioned (request) {
399
+ const url = new URL(request.url)
400
+ const { hostname, pathname: rawPathname, searchParams } = url
401
+ const pathname = decodeURI(rawPathname)
402
+
403
+ const accept = request.headers.get('Accept') || ''
404
+ const isRanged = request.headers.get('Range') || ''
405
+ const noResolve = searchParams.has('noResolve')
406
+
407
+ const parts = pathname.split('/')
408
+ const version = parts[3]
409
+ const realPath = parts.slice(4).join('/')
410
+
411
+ const drive = await getDrive(`hyper://${hostname}`)
412
+
413
+ const snapshot = await drive.checkout(version)
414
+
415
+ return serveHead(snapshot, realPath, { accept, isRanged, noResolve })
416
+ }
417
+
418
+ async function headFiles (request) {
377
419
  const url = new URL(request.url)
378
420
  const { hostname, pathname: rawPathname, searchParams } = url
379
421
  const pathname = decodeURI(rawPathname)
@@ -381,9 +423,14 @@ export default async function makeHyperFetch ({
381
423
  const accept = request.headers.get('Accept') || ''
382
424
  const isRanged = request.headers.get('Range') || ''
383
425
  const noResolve = searchParams.has('noResolve')
384
- const isDirectory = pathname.endsWith('/')
385
426
 
386
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('/')
387
434
  const fullURL = new URL(pathname, drive.url).href
388
435
 
389
436
  const resHeaders = {
@@ -478,19 +525,45 @@ 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
- router.get('hyper://*/**', async function getFiles (request) {
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}`)
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('/')
494
567
  const fullURL = new URL(pathname, drive.url).href
495
568
 
496
569
  if (isDirectory) {
@@ -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(request.headers, drive, posix.join(pathname, 'index.html'))
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(url, entries, fetch)
595
+ const body = await renderIndex(new URL(fullURL), entries, fetch)
523
596
  return {
524
597
  status: 200,
525
598
  body,
@@ -546,14 +619,13 @@ export default async function makeHyperFetch ({
546
619
  return { status: 404, body: 'Not Found' }
547
620
  }
548
621
 
549
- return serveFile(request.headers, drive, path)
550
- })
622
+ return serveFile(drive, path, isRanged)
623
+ }
551
624
 
552
625
  return fetch
553
626
  }
554
627
 
555
- async function serveFile (headers, drive, pathname) {
556
- const isRanged = headers.get('Range') || ''
628
+ async function serveFile (drive, pathname, isRanged) {
557
629
  const contentType = getMimeType(pathname)
558
630
 
559
631
  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.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",
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
  }