hypercore-fetch 9.3.1 → 9.5.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 +17 -0
- package/index.js +63 -22
- package/package.json +1 -1
- package/test.js +115 -7
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.
|
|
@@ -200,3 +212,8 @@ You can get older views of data in an archive by using the special `/$/version`
|
|
|
200
212
|
From there, you can use `GET` and `HEAD` requests with allt he same headers and querystring paramters as non-versioned paths to data.
|
|
201
213
|
|
|
202
214
|
Note that you cannot `PUT` or `DELETE` data in a versioned folder.
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## Limitations:
|
|
218
|
+
|
|
219
|
+
- 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 = [
|
|
@@ -79,7 +89,9 @@ export default async function makeHyperFetch ({
|
|
|
79
89
|
router.get(`hyper://${SPECIAL_DOMAIN}/`, getKey)
|
|
80
90
|
router.post(`hyper://${SPECIAL_DOMAIN}/`, createKey)
|
|
81
91
|
|
|
92
|
+
router.put(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, putFilesVersioned)
|
|
82
93
|
router.put('hyper://*/**', putFiles)
|
|
94
|
+
router.delete(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, deleteFilesVersioned)
|
|
83
95
|
router.delete('hyper://*/**', deleteFiles)
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -244,7 +256,7 @@ export default async function makeHyperFetch ({
|
|
|
244
256
|
|
|
245
257
|
async function listenExtension (request) {
|
|
246
258
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
247
|
-
const pathname = decodeURI(rawPathname)
|
|
259
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
248
260
|
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
249
261
|
|
|
250
262
|
const core = await getCore(`hyper://${hostname}/`)
|
|
@@ -266,7 +278,7 @@ export default async function makeHyperFetch ({
|
|
|
266
278
|
|
|
267
279
|
async function broadcastExtension (request) {
|
|
268
280
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
269
|
-
const pathname = decodeURI(rawPathname)
|
|
281
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
270
282
|
|
|
271
283
|
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
272
284
|
|
|
@@ -281,7 +293,7 @@ export default async function makeHyperFetch ({
|
|
|
281
293
|
|
|
282
294
|
async function extensionToPeer (request) {
|
|
283
295
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
284
|
-
const pathname = decodeURI(rawPathname)
|
|
296
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
285
297
|
|
|
286
298
|
const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
287
299
|
const [name, extensionPeer] = subFolder.split('/')
|
|
@@ -345,12 +357,16 @@ export default async function makeHyperFetch ({
|
|
|
345
357
|
|
|
346
358
|
async function putFiles (request) {
|
|
347
359
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
348
|
-
const pathname = decodeURI(rawPathname)
|
|
360
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
349
361
|
const contentType = request.headers.get('Content-Type') || ''
|
|
350
362
|
const isFormData = contentType.includes('multipart/form-data')
|
|
351
363
|
|
|
352
364
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
353
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
|
+
|
|
354
370
|
if (isFormData) {
|
|
355
371
|
// It's a form! Get the files out and process them
|
|
356
372
|
const formData = await request.formData()
|
|
@@ -367,25 +383,37 @@ export default async function makeHyperFetch ({
|
|
|
367
383
|
)
|
|
368
384
|
}
|
|
369
385
|
} else {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
386
|
+
if (pathname.endsWith('/')) {
|
|
387
|
+
return { status: 405, body: 'Cannot PUT file with trailing slash', headers: { Location: request.url } }
|
|
388
|
+
} else {
|
|
389
|
+
await pipelinePromise(
|
|
390
|
+
Readable.from(request.body),
|
|
391
|
+
drive.createWriteStream(pathname, {
|
|
392
|
+
metadata: {
|
|
393
|
+
mtime: Date.now()
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
)
|
|
397
|
+
}
|
|
378
398
|
}
|
|
379
399
|
|
|
380
400
|
return { status: 201, headers: { Location: request.url } }
|
|
381
401
|
}
|
|
382
402
|
|
|
403
|
+
function putFilesVersioned (request) {
|
|
404
|
+
return { status: 405, body: 'Cannot PUT file to old version', headers: { Location: request.url } }
|
|
405
|
+
}
|
|
406
|
+
|
|
383
407
|
async function deleteFiles (request) {
|
|
384
408
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
385
|
-
const pathname = decodeURI(rawPathname)
|
|
409
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
386
410
|
|
|
387
411
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
388
412
|
|
|
413
|
+
if (!drive.db.feed.writable) {
|
|
414
|
+
return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
415
|
+
}
|
|
416
|
+
|
|
389
417
|
if (pathname.endsWith('/')) {
|
|
390
418
|
let didDelete = false
|
|
391
419
|
for await (const entry of drive.list(pathname)) {
|
|
@@ -408,10 +436,14 @@ export default async function makeHyperFetch ({
|
|
|
408
436
|
return { status: 200 }
|
|
409
437
|
}
|
|
410
438
|
|
|
439
|
+
function deleteFilesVersioned (request) {
|
|
440
|
+
return { status: 405, body: 'Cannot DELETE old version', headers: { Location: request.url } }
|
|
441
|
+
}
|
|
442
|
+
|
|
411
443
|
async function headFilesVersioned (request) {
|
|
412
444
|
const url = new URL(request.url)
|
|
413
445
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
414
|
-
const pathname = decodeURI(rawPathname)
|
|
446
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
415
447
|
|
|
416
448
|
const accept = request.headers.get('Accept') || ''
|
|
417
449
|
const isRanged = request.headers.get('Range') || ''
|
|
@@ -419,7 +451,7 @@ export default async function makeHyperFetch ({
|
|
|
419
451
|
|
|
420
452
|
const parts = pathname.split('/')
|
|
421
453
|
const version = parts[3]
|
|
422
|
-
const realPath = parts.slice(4).join('/')
|
|
454
|
+
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
423
455
|
|
|
424
456
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
425
457
|
|
|
@@ -431,7 +463,7 @@ export default async function makeHyperFetch ({
|
|
|
431
463
|
async function headFiles (request) {
|
|
432
464
|
const url = new URL(request.url)
|
|
433
465
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
434
|
-
const pathname = decodeURI(rawPathname)
|
|
466
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
435
467
|
|
|
436
468
|
const accept = request.headers.get('Accept') || ''
|
|
437
469
|
const isRanged = request.headers.get('Range') || ''
|
|
@@ -446,10 +478,15 @@ export default async function makeHyperFetch ({
|
|
|
446
478
|
const isDirectory = pathname.endsWith('/')
|
|
447
479
|
const fullURL = new URL(pathname, drive.url).href
|
|
448
480
|
|
|
481
|
+
const isWritable = writable && drive.db.feed.writable
|
|
482
|
+
|
|
483
|
+
const Allow = isWritable ? BASIC_METHODS.concat(WRITABLE_METHODS) : BASIC_METHODS
|
|
484
|
+
|
|
449
485
|
const resHeaders = {
|
|
450
486
|
ETag: `${drive.version}`,
|
|
451
487
|
'Accept-Ranges': 'bytes',
|
|
452
|
-
Link: `<${fullURL}>; rel="canonical"
|
|
488
|
+
Link: `<${fullURL}>; rel="canonical"`,
|
|
489
|
+
Allow
|
|
453
490
|
}
|
|
454
491
|
|
|
455
492
|
if (isDirectory) {
|
|
@@ -507,7 +544,7 @@ export default async function makeHyperFetch ({
|
|
|
507
544
|
return { status: 404, body: 'Not Found' }
|
|
508
545
|
}
|
|
509
546
|
|
|
510
|
-
resHeaders.ETag = `${entry.seq}`
|
|
547
|
+
resHeaders.ETag = `${entry.seq + 1}`
|
|
511
548
|
resHeaders['Content-Length'] = `${entry.value.blob.byteLength}`
|
|
512
549
|
|
|
513
550
|
const contentType = getMimeType(path)
|
|
@@ -546,7 +583,7 @@ export default async function makeHyperFetch ({
|
|
|
546
583
|
async function getFilesVersioned (request) {
|
|
547
584
|
const url = new URL(request.url)
|
|
548
585
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
549
|
-
const pathname = decodeURI(rawPathname)
|
|
586
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
550
587
|
|
|
551
588
|
const accept = request.headers.get('Accept') || ''
|
|
552
589
|
const isRanged = request.headers.get('Range') || ''
|
|
@@ -554,7 +591,7 @@ export default async function makeHyperFetch ({
|
|
|
554
591
|
|
|
555
592
|
const parts = pathname.split('/')
|
|
556
593
|
const version = parts[3]
|
|
557
|
-
const realPath = parts.slice(4).join('/')
|
|
594
|
+
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
558
595
|
|
|
559
596
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
560
597
|
|
|
@@ -567,7 +604,7 @@ export default async function makeHyperFetch ({
|
|
|
567
604
|
async function getFiles (request) {
|
|
568
605
|
const url = new URL(request.url)
|
|
569
606
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
570
|
-
const pathname = decodeURI(rawPathname)
|
|
607
|
+
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
571
608
|
|
|
572
609
|
const accept = request.headers.get('Accept') || ''
|
|
573
610
|
const isRanged = request.headers.get('Range') || ''
|
|
@@ -651,7 +688,7 @@ async function serveFile (drive, pathname, isRanged) {
|
|
|
651
688
|
const entry = await drive.entry(pathname)
|
|
652
689
|
|
|
653
690
|
const resHeaders = {
|
|
654
|
-
ETag: `${entry.seq}`,
|
|
691
|
+
ETag: `${entry.seq + 1}`,
|
|
655
692
|
[HEADER_CONTENT_TYPE]: contentType,
|
|
656
693
|
'Accept-Ranges': 'bytes',
|
|
657
694
|
Link: `<${fullURL}>; rel="canonical"`
|
|
@@ -749,3 +786,7 @@ function getMimeType (path) {
|
|
|
749
786
|
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
|
|
750
787
|
return mimeType
|
|
751
788
|
}
|
|
789
|
+
|
|
790
|
+
function ensureLeadingSlash (path) {
|
|
791
|
+
return path.startsWith('/') ? path : '/' + path
|
|
792
|
+
}
|
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.")
|
|
@@ -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://
|
|
470
|
+
const loadResponse = await fetch('hyper://blog.mauve.moe/?noResolve')
|
|
471
471
|
|
|
472
472
|
const entries = await loadResponse.json()
|
|
473
473
|
|
|
@@ -484,7 +484,7 @@ test('Error on invalid hostname', async (t) => {
|
|
|
484
484
|
}
|
|
485
485
|
})
|
|
486
486
|
|
|
487
|
-
test('
|
|
487
|
+
test('Old versions in VERSION folder', async (t) => {
|
|
488
488
|
const created = await nextURL(t)
|
|
489
489
|
|
|
490
490
|
const fileName = 'example.txt'
|
|
@@ -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,120 @@ 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
|
|
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
|
-
|
|
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')
|
|
508
515
|
|
|
509
|
-
|
|
516
|
+
// PUT on old version should fail
|
|
517
|
+
const putResponse = await fetch(versionFileURL, {
|
|
518
|
+
method: 'PUT',
|
|
519
|
+
body: SAMPLE_CONTENT
|
|
520
|
+
})
|
|
521
|
+
if (putResponse.ok) {
|
|
522
|
+
throw new Error('PUT old version of file should have failed')
|
|
523
|
+
} else {
|
|
524
|
+
t.equal(putResponse.status, 405, 'PUT old version returned status 405 Not Allowed')
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// DELETE on old version should fail
|
|
528
|
+
const deleteResponse = await fetch(versionFileURL, {
|
|
529
|
+
method: 'delete'
|
|
530
|
+
})
|
|
531
|
+
if (deleteResponse.ok) {
|
|
532
|
+
throw new Error('DELETE old version of file should have failed')
|
|
533
|
+
} else {
|
|
534
|
+
t.equal(deleteResponse.status, 405, 'DELETE old version returned status 405 Not Allowed')
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
test('Handle empty string pathname', async (t) => {
|
|
539
|
+
const created = await nextURL(t)
|
|
540
|
+
const urlObject = new URL('', created)
|
|
541
|
+
const urlNoTrailingSlash = urlObject.href.slice(0, -1)
|
|
542
|
+
const versionedURLObject = new URL('/$/version/3/', created)
|
|
543
|
+
const versionedURLNoTrailingSlash = versionedURLObject.href.slice(0, -1)
|
|
544
|
+
|
|
545
|
+
// PUT
|
|
546
|
+
const putResponse = await fetch(urlNoTrailingSlash, { method: 'PUT', body: SAMPLE_CONTENT })
|
|
547
|
+
if (putResponse.ok) {
|
|
548
|
+
throw new Error('PUT file at the root directory should have failed')
|
|
549
|
+
} else {
|
|
550
|
+
t.pass('PUT file at root directory threw an error')
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// PUT FormData
|
|
554
|
+
const formData = new FormData()
|
|
555
|
+
formData.append('file', new Blob([SAMPLE_CONTENT]), 'example.txt')
|
|
556
|
+
formData.append('file', new Blob([SAMPLE_CONTENT]), 'example2.txt')
|
|
557
|
+
|
|
558
|
+
await checkResponse(
|
|
559
|
+
await fetch(urlNoTrailingSlash, {
|
|
560
|
+
method: 'put',
|
|
561
|
+
body: formData
|
|
562
|
+
}), t
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
// DELETE
|
|
566
|
+
await checkResponse(await fetch(urlNoTrailingSlash, { method: 'DELETE' }), t)
|
|
567
|
+
|
|
568
|
+
// HEAD
|
|
569
|
+
const headResponse = await fetch(urlNoTrailingSlash, { method: 'HEAD' })
|
|
570
|
+
await checkResponse(headResponse, t)
|
|
571
|
+
t.deepEqual(headResponse.headers.get('Etag'), '5', 'HEAD request returns correct Etag')
|
|
572
|
+
|
|
573
|
+
// HEAD (versioned)
|
|
574
|
+
const versionedHeadResponse = await fetch(versionedURLNoTrailingSlash, { method: 'HEAD' })
|
|
575
|
+
await checkResponse(versionedHeadResponse, t)
|
|
576
|
+
t.deepEqual(versionedHeadResponse.headers.get('Etag'), '3', 'Versioned HEAD request returns correct Etag')
|
|
577
|
+
|
|
578
|
+
// GET
|
|
579
|
+
const getResponse = await fetch(urlNoTrailingSlash)
|
|
580
|
+
await checkResponse(getResponse, t)
|
|
581
|
+
t.deepEqual(await getResponse.json(), [], 'Returns empty root directory')
|
|
582
|
+
|
|
583
|
+
// GET (versioned)
|
|
584
|
+
const versionedGetResponse = await fetch(versionedURLNoTrailingSlash)
|
|
585
|
+
await checkResponse(versionedGetResponse, t)
|
|
586
|
+
t.deepEqual(await versionedGetResponse.json(), ['example.txt', 'example2.txt'], 'Returns root directory prior to DELETE')
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
test('Return status 403 Forbidden on attempt to modify read-only hyperdrive', async (t) => {
|
|
590
|
+
const readOnlyURL = 'hyper://blog.mauve.moe/new-file.txt'
|
|
591
|
+
const putResponse = await fetch(readOnlyURL, { method: 'PUT', body: SAMPLE_CONTENT })
|
|
592
|
+
if (putResponse.ok) {
|
|
593
|
+
throw new Error('PUT file to read-only drive should have failed')
|
|
594
|
+
} else {
|
|
595
|
+
t.equal(putResponse.status, 403, 'PUT file to read-only drive returned status 403 Forbidden')
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const deleteResponse = await fetch(readOnlyURL, { method: 'DELETE' })
|
|
599
|
+
if (deleteResponse.ok) {
|
|
600
|
+
throw new Error('DELETE file in read-only drive should have failed')
|
|
601
|
+
} else {
|
|
602
|
+
t.equal(deleteResponse.status, 403, 'DELETE file to read-only drive returned status 403 Forbidden')
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test('Check hyperdrive writability', async (t) => {
|
|
607
|
+
const created = await nextURL(t)
|
|
510
608
|
|
|
511
|
-
|
|
609
|
+
const readOnlyRootDirectory = 'hyper://blog.mauve.moe/?noResolve'
|
|
610
|
+
const readOnlyHeadResponse = await fetch(readOnlyRootDirectory, { method: 'HEAD' })
|
|
611
|
+
await checkResponse(readOnlyHeadResponse, t, 'Able to load HEAD')
|
|
612
|
+
const readOnlyHeadersAllow = readOnlyHeadResponse.headers.get('Allow')
|
|
613
|
+
t.equal(readOnlyHeadersAllow, 'HEAD,GET', 'Expected read-only Allows header')
|
|
614
|
+
|
|
615
|
+
const writableRootDirectory = new URL('/', created)
|
|
616
|
+
const writableHeadResponse = await fetch(writableRootDirectory, { method: 'HEAD' })
|
|
617
|
+
await checkResponse(writableHeadResponse, t, 'Able to load HEAD')
|
|
618
|
+
const writableHeadersAllow = writableHeadResponse.headers.get('Allow')
|
|
619
|
+
t.equal(writableHeadersAllow, 'HEAD,GET,PUT,DELETE', 'Expected writable Allows header')
|
|
512
620
|
})
|
|
513
621
|
|
|
514
622
|
async function checkResponse (response, t, successMessage = 'Response OK') {
|