hypercore-fetch 9.4.0 → 9.6.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 +46 -6
- package/package.json +1 -1
- package/test.js +71 -1
package/README.md
CHANGED
|
@@ -53,6 +53,10 @@ Thus you can get the previous version of a file by using `hyper://NAME/$/version
|
|
|
53
53
|
|
|
54
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.
|
|
55
55
|
|
|
56
|
+
If the resource is a directory, it will contain the `Allow` header to
|
|
57
|
+
indicate whether a hyperdrive is writable (`'HEAD,GET'`) or not
|
|
58
|
+
(`'HEAD,GET,PUT,DELETE'`).
|
|
59
|
+
|
|
56
60
|
### `fetch('hyper://NAME/example.txt', {method: 'GET'})`
|
|
57
61
|
|
|
58
62
|
This will attempt to load `example.txt` from the archive labeled by `NAME`.
|
|
@@ -124,6 +128,9 @@ The `body` can be any of the options supported by the Fetch API such as a `Strin
|
|
|
124
128
|
|
|
125
129
|
Note that this is only available with the `writable: true` flag.
|
|
126
130
|
|
|
131
|
+
An attempt to `PUT` a file to a hyperdrive which is not writable will
|
|
132
|
+
fail with status `403`.
|
|
133
|
+
|
|
127
134
|
### `fetch('hyper://NAME/folder/', {method: 'PUT', body: new FormData()})`
|
|
128
135
|
|
|
129
136
|
You can add multiple files to a folder using the `PUT` method with a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) body.
|
|
@@ -141,6 +148,11 @@ You can delete a file or directory tree in a Hyperdrive by using the `DELETE` me
|
|
|
141
148
|
|
|
142
149
|
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive or Hypercore , or a domain to parse with the [DNSLink](https://www.dnslink.io/) standard.
|
|
143
150
|
|
|
151
|
+
Note that this is only available with the `writable: true` flag.
|
|
152
|
+
|
|
153
|
+
An attempt to `DELETE` a file in a hyperdrive which is not writable
|
|
154
|
+
will fail with status `403`.
|
|
155
|
+
|
|
144
156
|
### `fetch('hyper://NAME/$/extensions/')`
|
|
145
157
|
|
|
146
158
|
You can list the current [hypercore extensions](https://github.com/hypercore-protocol/hypercore#ext--feedregisterextensionname-handlers) that are enabled by doing a `GET` on the `/$/extensions/` directory.
|
package/index.js
CHANGED
|
@@ -89,7 +89,9 @@ export default async function makeHyperFetch ({
|
|
|
89
89
|
router.get(`hyper://${SPECIAL_DOMAIN}/`, getKey)
|
|
90
90
|
router.post(`hyper://${SPECIAL_DOMAIN}/`, createKey)
|
|
91
91
|
|
|
92
|
+
router.put(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, putFilesVersioned)
|
|
92
93
|
router.put('hyper://*/**', putFiles)
|
|
94
|
+
router.delete(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, deleteFilesVersioned)
|
|
93
95
|
router.delete('hyper://*/**', deleteFiles)
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -361,6 +363,12 @@ export default async function makeHyperFetch ({
|
|
|
361
363
|
|
|
362
364
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
363
365
|
|
|
366
|
+
if (!drive.db.feed.writable) {
|
|
367
|
+
return { status: 403, body: `Cannot PUT file to read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const mtime = Date.now()
|
|
371
|
+
|
|
364
372
|
if (isFormData) {
|
|
365
373
|
// It's a form! Get the files out and process them
|
|
366
374
|
const formData = await request.formData()
|
|
@@ -371,7 +379,7 @@ export default async function makeHyperFetch ({
|
|
|
371
379
|
Readable.from(data.stream()),
|
|
372
380
|
drive.createWriteStream(filePath, {
|
|
373
381
|
metadata: {
|
|
374
|
-
mtime
|
|
382
|
+
mtime
|
|
375
383
|
}
|
|
376
384
|
})
|
|
377
385
|
)
|
|
@@ -384,14 +392,31 @@ export default async function makeHyperFetch ({
|
|
|
384
392
|
Readable.from(request.body),
|
|
385
393
|
drive.createWriteStream(pathname, {
|
|
386
394
|
metadata: {
|
|
387
|
-
mtime
|
|
395
|
+
mtime
|
|
388
396
|
}
|
|
389
397
|
})
|
|
390
398
|
)
|
|
391
399
|
}
|
|
392
400
|
}
|
|
393
401
|
|
|
394
|
-
|
|
402
|
+
const fullURL = new URL(pathname, drive.url).href
|
|
403
|
+
|
|
404
|
+
const headers = {
|
|
405
|
+
Location: request.url,
|
|
406
|
+
ETag: `${drive.version}`,
|
|
407
|
+
Link: `<${fullURL}>; rel="canonical"`,
|
|
408
|
+
[HEADER_LAST_MODIFIED]: isFormData ? undefined : new Date(mtime).toUTCString()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { status: 201, headers }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function putFilesVersioned (request) {
|
|
415
|
+
return {
|
|
416
|
+
status: 405,
|
|
417
|
+
body: 'Cannot PUT file to old version',
|
|
418
|
+
headers: { Location: request.url }
|
|
419
|
+
}
|
|
395
420
|
}
|
|
396
421
|
|
|
397
422
|
async function deleteFiles (request) {
|
|
@@ -400,6 +425,10 @@ export default async function makeHyperFetch ({
|
|
|
400
425
|
|
|
401
426
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
402
427
|
|
|
428
|
+
if (!drive.db.feed.writable) {
|
|
429
|
+
return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
430
|
+
}
|
|
431
|
+
|
|
403
432
|
if (pathname.endsWith('/')) {
|
|
404
433
|
let didDelete = false
|
|
405
434
|
for await (const entry of drive.list(pathname)) {
|
|
@@ -419,7 +448,18 @@ export default async function makeHyperFetch ({
|
|
|
419
448
|
}
|
|
420
449
|
await drive.del(pathname)
|
|
421
450
|
|
|
422
|
-
|
|
451
|
+
const fullURL = new URL(pathname, drive.url).href
|
|
452
|
+
|
|
453
|
+
const headers = {
|
|
454
|
+
ETag: `${drive.version}`,
|
|
455
|
+
Link: `<${fullURL}>; rel="canonical"`
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { status: 200, headers }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function deleteFilesVersioned (request) {
|
|
462
|
+
return { status: 405, body: 'Cannot DELETE old version', headers: { Location: request.url } }
|
|
423
463
|
}
|
|
424
464
|
|
|
425
465
|
async function headFilesVersioned (request) {
|
|
@@ -460,7 +500,7 @@ export default async function makeHyperFetch ({
|
|
|
460
500
|
const isDirectory = pathname.endsWith('/')
|
|
461
501
|
const fullURL = new URL(pathname, drive.url).href
|
|
462
502
|
|
|
463
|
-
const isWritable = writable && drive.writable
|
|
503
|
+
const isWritable = writable && drive.db.feed.writable
|
|
464
504
|
|
|
465
505
|
const Allow = isWritable ? BASIC_METHODS.concat(WRITABLE_METHODS) : BASIC_METHODS
|
|
466
506
|
|
|
@@ -723,7 +763,7 @@ function makeToTry (pathname) {
|
|
|
723
763
|
|
|
724
764
|
async function resolvePath (drive, pathname, noResolve) {
|
|
725
765
|
if (noResolve) {
|
|
726
|
-
const entry = drive.entry(pathname)
|
|
766
|
+
const entry = await drive.entry(pathname)
|
|
727
767
|
|
|
728
768
|
return { entry, path: pathname }
|
|
729
769
|
}
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -333,6 +333,22 @@ 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('Ensure that noResolve works with file paths', async (t) => {
|
|
337
|
+
const created = await nextURL(t)
|
|
338
|
+
const uploadLocation = new URL('./example.txt', created)
|
|
339
|
+
const uploadResponse = await fetch(uploadLocation, {
|
|
340
|
+
method: 'put',
|
|
341
|
+
body: SAMPLE_CONTENT
|
|
342
|
+
})
|
|
343
|
+
await checkResponse(uploadResponse, t)
|
|
344
|
+
|
|
345
|
+
const noResolve = uploadLocation.href + '?noResolve'
|
|
346
|
+
const getRequest = await fetch(noResolve)
|
|
347
|
+
await checkResponse(getRequest, t)
|
|
348
|
+
|
|
349
|
+
const headRequest = await fetch(noResolve, { method: 'HEAD' })
|
|
350
|
+
await checkResponse(headRequest, t)
|
|
351
|
+
})
|
|
336
352
|
test('Render index.gmi', async (t) => {
|
|
337
353
|
const created = await nextURL(t)
|
|
338
354
|
const uploadLocation = new URL('./index.gmi', created)
|
|
@@ -484,7 +500,7 @@ test('Error on invalid hostname', async (t) => {
|
|
|
484
500
|
}
|
|
485
501
|
})
|
|
486
502
|
|
|
487
|
-
test('
|
|
503
|
+
test('Old versions in VERSION folder', async (t) => {
|
|
488
504
|
const created = await nextURL(t)
|
|
489
505
|
|
|
490
506
|
const fileName = 'example.txt'
|
|
@@ -512,6 +528,27 @@ test('GET older version of file from VERSION folder', async (t) => {
|
|
|
512
528
|
await checkResponse(versionedRootResponse, t, 'Able to GET versioned root')
|
|
513
529
|
const versionedRootContents = await versionedRootResponse.json()
|
|
514
530
|
t.deepEqual(versionedRootContents, [], 'Old root content got loaded')
|
|
531
|
+
|
|
532
|
+
// PUT on old version should fail
|
|
533
|
+
const putResponse = await fetch(versionFileURL, {
|
|
534
|
+
method: 'PUT',
|
|
535
|
+
body: SAMPLE_CONTENT
|
|
536
|
+
})
|
|
537
|
+
if (putResponse.ok) {
|
|
538
|
+
throw new Error('PUT old version of file should have failed')
|
|
539
|
+
} else {
|
|
540
|
+
t.equal(putResponse.status, 405, 'PUT old version returned status 405 Not Allowed')
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// DELETE on old version should fail
|
|
544
|
+
const deleteResponse = await fetch(versionFileURL, {
|
|
545
|
+
method: 'delete'
|
|
546
|
+
})
|
|
547
|
+
if (deleteResponse.ok) {
|
|
548
|
+
throw new Error('DELETE old version of file should have failed')
|
|
549
|
+
} else {
|
|
550
|
+
t.equal(deleteResponse.status, 405, 'DELETE old version returned status 405 Not Allowed')
|
|
551
|
+
}
|
|
515
552
|
})
|
|
516
553
|
|
|
517
554
|
test('Handle empty string pathname', async (t) => {
|
|
@@ -565,6 +602,39 @@ test('Handle empty string pathname', async (t) => {
|
|
|
565
602
|
t.deepEqual(await versionedGetResponse.json(), ['example.txt', 'example2.txt'], 'Returns root directory prior to DELETE')
|
|
566
603
|
})
|
|
567
604
|
|
|
605
|
+
test('Return status 403 Forbidden on attempt to modify read-only hyperdrive', async (t) => {
|
|
606
|
+
const readOnlyURL = 'hyper://blog.mauve.moe/new-file.txt'
|
|
607
|
+
const putResponse = await fetch(readOnlyURL, { method: 'PUT', body: SAMPLE_CONTENT })
|
|
608
|
+
if (putResponse.ok) {
|
|
609
|
+
throw new Error('PUT file to read-only drive should have failed')
|
|
610
|
+
} else {
|
|
611
|
+
t.equal(putResponse.status, 403, 'PUT file to read-only drive returned status 403 Forbidden')
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const deleteResponse = await fetch(readOnlyURL, { method: 'DELETE' })
|
|
615
|
+
if (deleteResponse.ok) {
|
|
616
|
+
throw new Error('DELETE file in read-only drive should have failed')
|
|
617
|
+
} else {
|
|
618
|
+
t.equal(deleteResponse.status, 403, 'DELETE file to read-only drive returned status 403 Forbidden')
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
test('Check hyperdrive writability', async (t) => {
|
|
623
|
+
const created = await nextURL(t)
|
|
624
|
+
|
|
625
|
+
const readOnlyRootDirectory = 'hyper://blog.mauve.moe/?noResolve'
|
|
626
|
+
const readOnlyHeadResponse = await fetch(readOnlyRootDirectory, { method: 'HEAD' })
|
|
627
|
+
await checkResponse(readOnlyHeadResponse, t, 'Able to load HEAD')
|
|
628
|
+
const readOnlyHeadersAllow = readOnlyHeadResponse.headers.get('Allow')
|
|
629
|
+
t.equal(readOnlyHeadersAllow, 'HEAD,GET', 'Expected read-only Allows header')
|
|
630
|
+
|
|
631
|
+
const writableRootDirectory = new URL('/', created)
|
|
632
|
+
const writableHeadResponse = await fetch(writableRootDirectory, { method: 'HEAD' })
|
|
633
|
+
await checkResponse(writableHeadResponse, t, 'Able to load HEAD')
|
|
634
|
+
const writableHeadersAllow = writableHeadResponse.headers.get('Allow')
|
|
635
|
+
t.equal(writableHeadersAllow, 'HEAD,GET,PUT,DELETE', 'Expected writable Allows header')
|
|
636
|
+
})
|
|
637
|
+
|
|
568
638
|
async function checkResponse (response, t, successMessage = 'Response OK') {
|
|
569
639
|
if (!response.ok) {
|
|
570
640
|
const message = await response.text()
|