hypercore-fetch 9.3.1 → 9.4.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 +5 -0
  2. package/index.js +45 -22
  3. package/package.json +1 -1
  4. package/test.js +60 -6
package/README.md CHANGED
@@ -200,3 +200,8 @@ You can get older views of data in an archive by using the special `/$/version`
200
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
201
 
202
202
  Note that you cannot `PUT` or `DELETE` data in a versioned folder.
203
+
204
+
205
+ ## Limitations:
206
+
207
+ - Since we make use of the special directory `$`, you cannot store files in this folder. If this is a major blocker, feel free to open an issue with alternative folder names we should consider.
package/index.js CHANGED
@@ -25,6 +25,16 @@ const MIME_EVENT_STREAM = 'text/event-stream; charset=utf-8'
25
25
  const HEADER_CONTENT_TYPE = 'Content-Type'
26
26
  const HEADER_LAST_MODIFIED = 'Last-Modified'
27
27
 
28
+ const WRITABLE_METHODS = [
29
+ 'PUT',
30
+ 'DELETE'
31
+ ]
32
+
33
+ const BASIC_METHODS = [
34
+ 'HEAD',
35
+ 'GET'
36
+ ]
37
+
28
38
  export const ERROR_KEY_NOT_CREATED = 'Must create key with POST before reading'
29
39
 
30
40
  const INDEX_FILES = [
@@ -244,7 +254,7 @@ export default async function makeHyperFetch ({
244
254
 
245
255
  async function listenExtension (request) {
246
256
  const { hostname, pathname: rawPathname } = new URL(request.url)
247
- const pathname = decodeURI(rawPathname)
257
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
248
258
  const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
249
259
 
250
260
  const core = await getCore(`hyper://${hostname}/`)
@@ -266,7 +276,7 @@ export default async function makeHyperFetch ({
266
276
 
267
277
  async function broadcastExtension (request) {
268
278
  const { hostname, pathname: rawPathname } = new URL(request.url)
269
- const pathname = decodeURI(rawPathname)
279
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
270
280
 
271
281
  const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
272
282
 
@@ -281,7 +291,7 @@ export default async function makeHyperFetch ({
281
291
 
282
292
  async function extensionToPeer (request) {
283
293
  const { hostname, pathname: rawPathname } = new URL(request.url)
284
- const pathname = decodeURI(rawPathname)
294
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
285
295
 
286
296
  const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
287
297
  const [name, extensionPeer] = subFolder.split('/')
@@ -345,7 +355,7 @@ export default async function makeHyperFetch ({
345
355
 
346
356
  async function putFiles (request) {
347
357
  const { hostname, pathname: rawPathname } = new URL(request.url)
348
- const pathname = decodeURI(rawPathname)
358
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
349
359
  const contentType = request.headers.get('Content-Type') || ''
350
360
  const isFormData = contentType.includes('multipart/form-data')
351
361
 
@@ -367,14 +377,18 @@ export default async function makeHyperFetch ({
367
377
  )
368
378
  }
369
379
  } else {
370
- await pipelinePromise(
371
- Readable.from(request.body),
372
- drive.createWriteStream(pathname, {
373
- metadata: {
374
- mtime: Date.now()
375
- }
376
- })
377
- )
380
+ if (pathname.endsWith('/')) {
381
+ return { status: 405, body: 'Cannot PUT file with trailing slash', headers: { Location: request.url } }
382
+ } else {
383
+ await pipelinePromise(
384
+ Readable.from(request.body),
385
+ drive.createWriteStream(pathname, {
386
+ metadata: {
387
+ mtime: Date.now()
388
+ }
389
+ })
390
+ )
391
+ }
378
392
  }
379
393
 
380
394
  return { status: 201, headers: { Location: request.url } }
@@ -382,7 +396,7 @@ export default async function makeHyperFetch ({
382
396
 
383
397
  async function deleteFiles (request) {
384
398
  const { hostname, pathname: rawPathname } = new URL(request.url)
385
- const pathname = decodeURI(rawPathname)
399
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
386
400
 
387
401
  const drive = await getDrive(`hyper://${hostname}`)
388
402
 
@@ -411,7 +425,7 @@ export default async function makeHyperFetch ({
411
425
  async function headFilesVersioned (request) {
412
426
  const url = new URL(request.url)
413
427
  const { hostname, pathname: rawPathname, searchParams } = url
414
- const pathname = decodeURI(rawPathname)
428
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
415
429
 
416
430
  const accept = request.headers.get('Accept') || ''
417
431
  const isRanged = request.headers.get('Range') || ''
@@ -419,7 +433,7 @@ export default async function makeHyperFetch ({
419
433
 
420
434
  const parts = pathname.split('/')
421
435
  const version = parts[3]
422
- const realPath = parts.slice(4).join('/')
436
+ const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
423
437
 
424
438
  const drive = await getDrive(`hyper://${hostname}`)
425
439
 
@@ -431,7 +445,7 @@ export default async function makeHyperFetch ({
431
445
  async function headFiles (request) {
432
446
  const url = new URL(request.url)
433
447
  const { hostname, pathname: rawPathname, searchParams } = url
434
- const pathname = decodeURI(rawPathname)
448
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
435
449
 
436
450
  const accept = request.headers.get('Accept') || ''
437
451
  const isRanged = request.headers.get('Range') || ''
@@ -446,10 +460,15 @@ export default async function makeHyperFetch ({
446
460
  const isDirectory = pathname.endsWith('/')
447
461
  const fullURL = new URL(pathname, drive.url).href
448
462
 
463
+ const isWritable = writable && drive.writable
464
+
465
+ const Allow = isWritable ? BASIC_METHODS.concat(WRITABLE_METHODS) : BASIC_METHODS
466
+
449
467
  const resHeaders = {
450
468
  ETag: `${drive.version}`,
451
469
  'Accept-Ranges': 'bytes',
452
- Link: `<${fullURL}>; rel="canonical"`
470
+ Link: `<${fullURL}>; rel="canonical"`,
471
+ Allow
453
472
  }
454
473
 
455
474
  if (isDirectory) {
@@ -507,7 +526,7 @@ export default async function makeHyperFetch ({
507
526
  return { status: 404, body: 'Not Found' }
508
527
  }
509
528
 
510
- resHeaders.ETag = `${entry.seq}`
529
+ resHeaders.ETag = `${entry.seq + 1}`
511
530
  resHeaders['Content-Length'] = `${entry.value.blob.byteLength}`
512
531
 
513
532
  const contentType = getMimeType(path)
@@ -546,7 +565,7 @@ export default async function makeHyperFetch ({
546
565
  async function getFilesVersioned (request) {
547
566
  const url = new URL(request.url)
548
567
  const { hostname, pathname: rawPathname, searchParams } = url
549
- const pathname = decodeURI(rawPathname)
568
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
550
569
 
551
570
  const accept = request.headers.get('Accept') || ''
552
571
  const isRanged = request.headers.get('Range') || ''
@@ -554,7 +573,7 @@ export default async function makeHyperFetch ({
554
573
 
555
574
  const parts = pathname.split('/')
556
575
  const version = parts[3]
557
- const realPath = parts.slice(4).join('/')
576
+ const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
558
577
 
559
578
  const drive = await getDrive(`hyper://${hostname}`)
560
579
 
@@ -567,7 +586,7 @@ export default async function makeHyperFetch ({
567
586
  async function getFiles (request) {
568
587
  const url = new URL(request.url)
569
588
  const { hostname, pathname: rawPathname, searchParams } = url
570
- const pathname = decodeURI(rawPathname)
589
+ const pathname = decodeURI(ensureLeadingSlash(rawPathname))
571
590
 
572
591
  const accept = request.headers.get('Accept') || ''
573
592
  const isRanged = request.headers.get('Range') || ''
@@ -651,7 +670,7 @@ async function serveFile (drive, pathname, isRanged) {
651
670
  const entry = await drive.entry(pathname)
652
671
 
653
672
  const resHeaders = {
654
- ETag: `${entry.seq}`,
673
+ ETag: `${entry.seq + 1}`,
655
674
  [HEADER_CONTENT_TYPE]: contentType,
656
675
  'Accept-Ranges': 'bytes',
657
676
  Link: `<${fullURL}>; rel="canonical"`
@@ -749,3 +768,7 @@ function getMimeType (path) {
749
768
  if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
750
769
  return mimeType
751
770
  }
771
+
772
+ function ensureLeadingSlash (path) {
773
+ return path.startsWith('/') ? path : '/' + path
774
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypercore-fetch",
3
- "version": "9.3.1",
3
+ "version": "9.4.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
@@ -128,7 +128,7 @@ test('HEAD request', async (t) => {
128
128
  const headersLink = headResponse.headers.get('Link')
129
129
 
130
130
  // Version at which the file was added
131
- t.equal(headersEtag, '1', 'Headers got expected etag')
131
+ t.equal(headersEtag, '2', 'Headers got expected etag')
132
132
  t.equal(headersContentType, 'text/plain; charset=utf-8', 'Headers got expected mime type')
133
133
  t.ok(headersContentLength, "Headers have 'Content-Length' set.")
134
134
  t.ok(headersLastModified, "Headers have 'Last-Modified' set.")
@@ -467,7 +467,7 @@ test('EventSource extension messages', async (t) => {
467
467
  })
468
468
 
469
469
  test('Resolve DNS', async (t) => {
470
- const loadResponse = await fetch('hyper://example2.mauve.moe/?noResolve')
470
+ const loadResponse = await fetch('hyper://blog.mauve.moe/?noResolve')
471
471
 
472
472
  const entries = await loadResponse.json()
473
473
 
@@ -494,6 +494,7 @@ test('GET older version of file from VERSION folder', async (t) => {
494
494
 
495
495
  const fileURL = new URL(`/${fileName}`, created)
496
496
  const versionFileURL = new URL(`/$/version/2/${fileName}`, created)
497
+ const versionRootURL = new URL('/$/version/1/', created)
497
498
 
498
499
  await checkResponse(
499
500
  await fetch(fileURL, { method: 'PUT', body: data1 }), t
@@ -502,13 +503,66 @@ test('GET older version of file from VERSION folder', async (t) => {
502
503
  await fetch(fileURL, { method: 'PUT', body: data2 }), t
503
504
  )
504
505
 
505
- const versionedResponse = await fetch(versionFileURL)
506
+ const versionedFileResponse = await fetch(versionFileURL)
507
+ await checkResponse(versionedFileResponse, t, 'Able to GET versioned file')
508
+ const versionedFileData = await versionedFileResponse.text()
509
+ t.equal(versionedFileData, data1, 'Old data got loaded')
506
510
 
507
- await checkResponse(versionedResponse, t, 'Able to GET versioned file')
511
+ const versionedRootResponse = await fetch(versionRootURL)
512
+ await checkResponse(versionedRootResponse, t, 'Able to GET versioned root')
513
+ const versionedRootContents = await versionedRootResponse.json()
514
+ t.deepEqual(versionedRootContents, [], 'Old root content got loaded')
515
+ })
516
+
517
+ test('Handle empty string pathname', async (t) => {
518
+ const created = await nextURL(t)
519
+ const urlObject = new URL('', created)
520
+ const urlNoTrailingSlash = urlObject.href.slice(0, -1)
521
+ const versionedURLObject = new URL('/$/version/3/', created)
522
+ const versionedURLNoTrailingSlash = versionedURLObject.href.slice(0, -1)
523
+
524
+ // PUT
525
+ const putResponse = await fetch(urlNoTrailingSlash, { method: 'PUT', body: SAMPLE_CONTENT })
526
+ if (putResponse.ok) {
527
+ throw new Error('PUT file at the root directory should have failed')
528
+ } else {
529
+ t.pass('PUT file at root directory threw an error')
530
+ }
531
+
532
+ // PUT FormData
533
+ const formData = new FormData()
534
+ formData.append('file', new Blob([SAMPLE_CONTENT]), 'example.txt')
535
+ formData.append('file', new Blob([SAMPLE_CONTENT]), 'example2.txt')
536
+
537
+ await checkResponse(
538
+ await fetch(urlNoTrailingSlash, {
539
+ method: 'put',
540
+ body: formData
541
+ }), t
542
+ )
543
+
544
+ // DELETE
545
+ await checkResponse(await fetch(urlNoTrailingSlash, { method: 'DELETE' }), t)
546
+
547
+ // HEAD
548
+ const headResponse = await fetch(urlNoTrailingSlash, { method: 'HEAD' })
549
+ await checkResponse(headResponse, t)
550
+ t.deepEqual(headResponse.headers.get('Etag'), '5', 'HEAD request returns correct Etag')
551
+
552
+ // HEAD (versioned)
553
+ const versionedHeadResponse = await fetch(versionedURLNoTrailingSlash, { method: 'HEAD' })
554
+ await checkResponse(versionedHeadResponse, t)
555
+ t.deepEqual(versionedHeadResponse.headers.get('Etag'), '3', 'Versioned HEAD request returns correct Etag')
508
556
 
509
- const versionedData = await versionedResponse.text()
557
+ // GET
558
+ const getResponse = await fetch(urlNoTrailingSlash)
559
+ await checkResponse(getResponse, t)
560
+ t.deepEqual(await getResponse.json(), [], 'Returns empty root directory')
510
561
 
511
- t.equal(versionedData, data1, 'Old data got loaded')
562
+ // GET (versioned)
563
+ const versionedGetResponse = await fetch(versionedURLNoTrailingSlash)
564
+ await checkResponse(versionedGetResponse, t)
565
+ t.deepEqual(await versionedGetResponse.json(), ['example.txt', 'example2.txt'], 'Returns root directory prior to DELETE')
512
566
  })
513
567
 
514
568
  async function checkResponse (response, t, successMessage = 'Response OK') {