hypercore-fetch 9.3.0 → 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.
- package/README.md +5 -0
- package/index.js +46 -23
- package/package.json +1 -1
- package/test.js +80 -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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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') || ''
|
|
@@ -604,7 +623,7 @@ export default async function makeHyperFetch ({
|
|
|
604
623
|
if (!noResolve) {
|
|
605
624
|
for (const indexFile of INDEX_FILES) {
|
|
606
625
|
if (entries.includes(indexFile)) {
|
|
607
|
-
return serveFile(drive, posix.join(pathname,
|
|
626
|
+
return serveFile(drive, posix.join(pathname, indexFile), isRanged)
|
|
608
627
|
}
|
|
609
628
|
}
|
|
610
629
|
}
|
|
@@ -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
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, '
|
|
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.")
|
|
@@ -333,6 +333,26 @@ test('Ignore index.html with noResolve', async (t) => {
|
|
|
333
333
|
const entries = await listDirRequest.json()
|
|
334
334
|
t.deepEqual(entries, ['index.html'], 'able to list index.html')
|
|
335
335
|
})
|
|
336
|
+
test('Render index.gmi', async (t) => {
|
|
337
|
+
const created = await nextURL(t)
|
|
338
|
+
const uploadLocation = new URL('./index.gmi', created)
|
|
339
|
+
|
|
340
|
+
const uploadResponse = await fetch(uploadLocation, {
|
|
341
|
+
method: 'put',
|
|
342
|
+
body: SAMPLE_CONTENT
|
|
343
|
+
})
|
|
344
|
+
await checkResponse(uploadResponse, t)
|
|
345
|
+
|
|
346
|
+
const uploadedContentResponse = await fetch(created)
|
|
347
|
+
|
|
348
|
+
await checkResponse(uploadedContentResponse, t)
|
|
349
|
+
|
|
350
|
+
const content = await uploadedContentResponse.text()
|
|
351
|
+
const contentType = uploadedContentResponse.headers.get('Content-Type')
|
|
352
|
+
|
|
353
|
+
t.equal(contentType, 'text/gemini; charset=utf-8', 'got HTML mime type')
|
|
354
|
+
t.equal(content, SAMPLE_CONTENT, 'loaded index.html content')
|
|
355
|
+
})
|
|
336
356
|
test('Read directory as HTML', async (t) => {
|
|
337
357
|
const created = await nextURL(t)
|
|
338
358
|
|
|
@@ -447,7 +467,7 @@ test('EventSource extension messages', async (t) => {
|
|
|
447
467
|
})
|
|
448
468
|
|
|
449
469
|
test('Resolve DNS', async (t) => {
|
|
450
|
-
const loadResponse = await fetch('hyper://
|
|
470
|
+
const loadResponse = await fetch('hyper://blog.mauve.moe/?noResolve')
|
|
451
471
|
|
|
452
472
|
const entries = await loadResponse.json()
|
|
453
473
|
|
|
@@ -474,6 +494,7 @@ test('GET older version of file from VERSION folder', async (t) => {
|
|
|
474
494
|
|
|
475
495
|
const fileURL = new URL(`/${fileName}`, created)
|
|
476
496
|
const versionFileURL = new URL(`/$/version/2/${fileName}`, created)
|
|
497
|
+
const versionRootURL = new URL('/$/version/1/', created)
|
|
477
498
|
|
|
478
499
|
await checkResponse(
|
|
479
500
|
await fetch(fileURL, { method: 'PUT', body: data1 }), t
|
|
@@ -482,13 +503,66 @@ test('GET older version of file from VERSION folder', async (t) => {
|
|
|
482
503
|
await fetch(fileURL, { method: 'PUT', body: data2 }), t
|
|
483
504
|
)
|
|
484
505
|
|
|
485
|
-
const
|
|
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')
|
|
510
|
+
|
|
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')
|
|
486
551
|
|
|
487
|
-
|
|
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')
|
|
488
556
|
|
|
489
|
-
|
|
557
|
+
// GET
|
|
558
|
+
const getResponse = await fetch(urlNoTrailingSlash)
|
|
559
|
+
await checkResponse(getResponse, t)
|
|
560
|
+
t.deepEqual(await getResponse.json(), [], 'Returns empty root directory')
|
|
490
561
|
|
|
491
|
-
|
|
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')
|
|
492
566
|
})
|
|
493
567
|
|
|
494
568
|
async function checkResponse (response, t, successMessage = 'Response OK') {
|