hypercore-fetch 9.5.0 → 9.7.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 +10 -0
- package/index.js +88 -19
- package/package.json +2 -2
- package/test.js +41 -15
package/README.md
CHANGED
|
@@ -142,6 +142,16 @@ Note that you must use the name `file` for uploaded files.
|
|
|
142
142
|
|
|
143
143
|
`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.
|
|
144
144
|
|
|
145
|
+
### `fetch('hyper://NAME/', {method: 'DELETE'})`
|
|
146
|
+
|
|
147
|
+
You can purge all the stored data for a hyperdrive by sending a `DELETE` to it's root.
|
|
148
|
+
|
|
149
|
+
If this is a writable drive, your data will get fully clearned and trying to write to it again will lead to data corruption.
|
|
150
|
+
|
|
151
|
+
If you try to load this drive again data will be loaded from scratch.
|
|
152
|
+
|
|
153
|
+
`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.
|
|
154
|
+
|
|
145
155
|
### `fetch('hyper://NAME/example.txt', {method: 'DELETE'})`
|
|
146
156
|
|
|
147
157
|
You can delete a file or directory tree in a Hyperdrive by using the `DELETE` method.
|
package/index.js
CHANGED
|
@@ -36,13 +36,16 @@ const BASIC_METHODS = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
export const ERROR_KEY_NOT_CREATED = 'Must create key with POST before reading'
|
|
39
|
+
export const ERROR_DRIVE_EMPTY = 'Could not find data in drive, make sure your key is correct and that there are peers online to load data from'
|
|
39
40
|
|
|
40
41
|
const INDEX_FILES = [
|
|
41
42
|
'index.html',
|
|
42
43
|
'index.md',
|
|
43
44
|
'index.gmi',
|
|
44
45
|
'index.gemini',
|
|
45
|
-
'
|
|
46
|
+
'index.org',
|
|
47
|
+
'README.md',
|
|
48
|
+
'README.org'
|
|
46
49
|
]
|
|
47
50
|
|
|
48
51
|
async function DEFAULT_RENDER_INDEX (url, files, fetch) {
|
|
@@ -70,7 +73,9 @@ export default async function makeHyperFetch ({
|
|
|
70
73
|
timeout = DEFAULT_TIMEOUT,
|
|
71
74
|
renderIndex = DEFAULT_RENDER_INDEX
|
|
72
75
|
}) {
|
|
73
|
-
const { fetch, router } = makeRoutedFetch(
|
|
76
|
+
const { fetch, router } = makeRoutedFetch({
|
|
77
|
+
onError
|
|
78
|
+
})
|
|
74
79
|
|
|
75
80
|
// Map loaded drive hostnames to their keys
|
|
76
81
|
// TODO: Track LRU + cache clearing
|
|
@@ -91,6 +96,7 @@ export default async function makeHyperFetch ({
|
|
|
91
96
|
|
|
92
97
|
router.put(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, putFilesVersioned)
|
|
93
98
|
router.put('hyper://*/**', putFiles)
|
|
99
|
+
router.delete('hyper://*/', deleteDrive)
|
|
94
100
|
router.delete(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, deleteFilesVersioned)
|
|
95
101
|
router.delete('hyper://*/**', deleteFiles)
|
|
96
102
|
}
|
|
@@ -100,6 +106,16 @@ export default async function makeHyperFetch ({
|
|
|
100
106
|
router.get('hyper://*/**', getFiles)
|
|
101
107
|
router.head('hyper://*/**', headFiles)
|
|
102
108
|
|
|
109
|
+
async function onError (e, request) {
|
|
110
|
+
return {
|
|
111
|
+
status: e.statusCode || 500,
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'text/plain; charset=utf-8'
|
|
114
|
+
},
|
|
115
|
+
body: e.stack
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
103
119
|
async function getCore (hostname) {
|
|
104
120
|
if (cores.has(hostname)) {
|
|
105
121
|
return cores.get(hostname)
|
|
@@ -128,18 +144,30 @@ export default async function makeHyperFetch ({
|
|
|
128
144
|
return dbCore
|
|
129
145
|
}
|
|
130
146
|
|
|
131
|
-
async function getDrive (hostname) {
|
|
147
|
+
async function getDrive (hostname, errorOnNew = false) {
|
|
132
148
|
if (drives.has(hostname)) {
|
|
133
149
|
return drives.get(hostname)
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
const core = await getCore(hostname)
|
|
137
153
|
|
|
154
|
+
if (!core.length && errorOnNew) {
|
|
155
|
+
await core.close()
|
|
156
|
+
const e = new Error(ERROR_DRIVE_EMPTY)
|
|
157
|
+
e.statusCode = 404
|
|
158
|
+
throw e
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
const corestore = sdk.namespace(core.id)
|
|
139
162
|
const drive = new Hyperdrive(corestore, core.key)
|
|
140
163
|
|
|
141
164
|
await drive.ready()
|
|
142
165
|
|
|
166
|
+
drive.once('close', () => {
|
|
167
|
+
drives.delete(drive.core.id)
|
|
168
|
+
drives.delete(hostname)
|
|
169
|
+
})
|
|
170
|
+
|
|
143
171
|
drives.set(drive.core.id, drive)
|
|
144
172
|
drives.set(hostname, drive)
|
|
145
173
|
|
|
@@ -151,8 +179,11 @@ export default async function makeHyperFetch ({
|
|
|
151
179
|
return drives.get(key)
|
|
152
180
|
}
|
|
153
181
|
const core = await getDBCoreForName(key)
|
|
182
|
+
|
|
154
183
|
if (!core.length && errorOnNew) {
|
|
155
|
-
|
|
184
|
+
const e = new Error(ERROR_KEY_NOT_CREATED)
|
|
185
|
+
e.statusCode = 404
|
|
186
|
+
throw e
|
|
156
187
|
}
|
|
157
188
|
|
|
158
189
|
const corestore = sdk.namespace(key)
|
|
@@ -160,7 +191,14 @@ export default async function makeHyperFetch ({
|
|
|
160
191
|
|
|
161
192
|
await drive.ready()
|
|
162
193
|
|
|
194
|
+
drive.once('close', () => {
|
|
195
|
+
drives.delete(key)
|
|
196
|
+
drives.delete(drive.url)
|
|
197
|
+
drives.delete(drive.core.id)
|
|
198
|
+
})
|
|
199
|
+
|
|
163
200
|
drives.set(key, drive)
|
|
201
|
+
drives.set(drive.url, drive)
|
|
164
202
|
drives.set(drive.core.id, drive)
|
|
165
203
|
|
|
166
204
|
return drive
|
|
@@ -361,12 +399,14 @@ export default async function makeHyperFetch ({
|
|
|
361
399
|
const contentType = request.headers.get('Content-Type') || ''
|
|
362
400
|
const isFormData = contentType.includes('multipart/form-data')
|
|
363
401
|
|
|
364
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
402
|
+
const drive = await getDrive(`hyper://${hostname}/`, true)
|
|
365
403
|
|
|
366
404
|
if (!drive.db.feed.writable) {
|
|
367
405
|
return { status: 403, body: `Cannot PUT file to read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
368
406
|
}
|
|
369
407
|
|
|
408
|
+
const mtime = Date.now()
|
|
409
|
+
|
|
370
410
|
if (isFormData) {
|
|
371
411
|
// It's a form! Get the files out and process them
|
|
372
412
|
const formData = await request.formData()
|
|
@@ -377,7 +417,7 @@ export default async function makeHyperFetch ({
|
|
|
377
417
|
Readable.from(data.stream()),
|
|
378
418
|
drive.createWriteStream(filePath, {
|
|
379
419
|
metadata: {
|
|
380
|
-
mtime
|
|
420
|
+
mtime
|
|
381
421
|
}
|
|
382
422
|
})
|
|
383
423
|
)
|
|
@@ -390,25 +430,47 @@ export default async function makeHyperFetch ({
|
|
|
390
430
|
Readable.from(request.body),
|
|
391
431
|
drive.createWriteStream(pathname, {
|
|
392
432
|
metadata: {
|
|
393
|
-
mtime
|
|
433
|
+
mtime
|
|
394
434
|
}
|
|
395
435
|
})
|
|
396
436
|
)
|
|
397
437
|
}
|
|
398
438
|
}
|
|
399
439
|
|
|
400
|
-
|
|
440
|
+
const fullURL = new URL(pathname, drive.url).href
|
|
441
|
+
|
|
442
|
+
const headers = {
|
|
443
|
+
Location: request.url,
|
|
444
|
+
ETag: `${drive.version}`,
|
|
445
|
+
Link: `<${fullURL}>; rel="canonical"`,
|
|
446
|
+
[HEADER_LAST_MODIFIED]: isFormData ? undefined : new Date(mtime).toUTCString()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return { status: 201, headers }
|
|
401
450
|
}
|
|
402
451
|
|
|
403
452
|
function putFilesVersioned (request) {
|
|
404
|
-
return {
|
|
453
|
+
return {
|
|
454
|
+
status: 405,
|
|
455
|
+
body: 'Cannot PUT file to old version',
|
|
456
|
+
headers: { Location: request.url }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function deleteDrive (request) {
|
|
461
|
+
const { hostname } = new URL(request.url)
|
|
462
|
+
const drive = await getDrive(`hyper://${hostname}/`, true)
|
|
463
|
+
|
|
464
|
+
await drive.purge()
|
|
465
|
+
|
|
466
|
+
return { status: 200 }
|
|
405
467
|
}
|
|
406
468
|
|
|
407
469
|
async function deleteFiles (request) {
|
|
408
470
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
409
471
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
410
472
|
|
|
411
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
473
|
+
const drive = await getDrive(`hyper://${hostname}/`, true)
|
|
412
474
|
|
|
413
475
|
if (!drive.db.feed.writable) {
|
|
414
476
|
return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
@@ -433,7 +495,14 @@ export default async function makeHyperFetch ({
|
|
|
433
495
|
}
|
|
434
496
|
await drive.del(pathname)
|
|
435
497
|
|
|
436
|
-
|
|
498
|
+
const fullURL = new URL(pathname, drive.url).href
|
|
499
|
+
|
|
500
|
+
const headers = {
|
|
501
|
+
ETag: `${drive.version}`,
|
|
502
|
+
Link: `<${fullURL}>; rel="canonical"`
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { status: 200, headers }
|
|
437
506
|
}
|
|
438
507
|
|
|
439
508
|
function deleteFilesVersioned (request) {
|
|
@@ -453,7 +522,7 @@ export default async function makeHyperFetch ({
|
|
|
453
522
|
const version = parts[3]
|
|
454
523
|
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
455
524
|
|
|
456
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
525
|
+
const drive = await getDrive(`hyper://${hostname}/`, true)
|
|
457
526
|
|
|
458
527
|
const snapshot = await drive.checkout(version)
|
|
459
528
|
|
|
@@ -469,7 +538,7 @@ export default async function makeHyperFetch ({
|
|
|
469
538
|
const isRanged = request.headers.get('Range') || ''
|
|
470
539
|
const noResolve = searchParams.has('noResolve')
|
|
471
540
|
|
|
472
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
541
|
+
const drive = await getDrive(`hyper://${hostname}/`, true)
|
|
473
542
|
|
|
474
543
|
return serveHead(drive, pathname, { accept, isRanged, noResolve })
|
|
475
544
|
}
|
|
@@ -555,7 +624,7 @@ export default async function makeHyperFetch ({
|
|
|
555
624
|
resHeaders[HEADER_LAST_MODIFIED] = date.toUTCString()
|
|
556
625
|
}
|
|
557
626
|
|
|
558
|
-
const size = entry.value.byteLength
|
|
627
|
+
const size = entry.value.blob.byteLength
|
|
559
628
|
if (isRanged) {
|
|
560
629
|
const ranges = parseRange(size, isRanged)
|
|
561
630
|
|
|
@@ -564,7 +633,7 @@ export default async function makeHyperFetch ({
|
|
|
564
633
|
const length = (end - start + 1)
|
|
565
634
|
|
|
566
635
|
return {
|
|
567
|
-
status:
|
|
636
|
+
status: 204,
|
|
568
637
|
headers: {
|
|
569
638
|
...resHeaders,
|
|
570
639
|
'Content-Length': `${length}`,
|
|
@@ -575,7 +644,7 @@ export default async function makeHyperFetch ({
|
|
|
575
644
|
}
|
|
576
645
|
|
|
577
646
|
return {
|
|
578
|
-
status:
|
|
647
|
+
status: 204,
|
|
579
648
|
headers: resHeaders
|
|
580
649
|
}
|
|
581
650
|
}
|
|
@@ -593,7 +662,7 @@ export default async function makeHyperFetch ({
|
|
|
593
662
|
const version = parts[3]
|
|
594
663
|
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
595
664
|
|
|
596
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
665
|
+
const drive = await getDrive(`hyper://${hostname}/`, true)
|
|
597
666
|
|
|
598
667
|
const snapshot = await drive.checkout(version)
|
|
599
668
|
|
|
@@ -610,7 +679,7 @@ export default async function makeHyperFetch ({
|
|
|
610
679
|
const isRanged = request.headers.get('Range') || ''
|
|
611
680
|
const noResolve = searchParams.has('noResolve')
|
|
612
681
|
|
|
613
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
682
|
+
const drive = await getDrive(`hyper://${hostname}/`, true)
|
|
614
683
|
|
|
615
684
|
return serveGet(drive, pathname, { accept, isRanged, noResolve })
|
|
616
685
|
}
|
|
@@ -741,7 +810,7 @@ function makeToTry (pathname) {
|
|
|
741
810
|
|
|
742
811
|
async function resolvePath (drive, pathname, noResolve) {
|
|
743
812
|
if (noResolve) {
|
|
744
|
-
const entry = drive.entry(pathname)
|
|
813
|
+
const entry = await drive.entry(pathname)
|
|
745
814
|
|
|
746
815
|
return { entry, path: pathname }
|
|
747
816
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore-fetch",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.7.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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@rangermauve/fetch-event-source": "^1.0.3",
|
|
35
|
-
"hyper-sdk": "^4.
|
|
35
|
+
"hyper-sdk": "^4.3.0",
|
|
36
36
|
"standard": "^17.0.0",
|
|
37
37
|
"tape": "^5.2.2"
|
|
38
38
|
}
|
package/test.js
CHANGED
|
@@ -127,6 +127,7 @@ test('HEAD request', async (t) => {
|
|
|
127
127
|
const headersLastModified = headResponse.headers.get('Last-Modified')
|
|
128
128
|
const headersLink = headResponse.headers.get('Link')
|
|
129
129
|
|
|
130
|
+
t.equal(headResponse.status, 204, 'Response had expected status')
|
|
130
131
|
// Version at which the file was added
|
|
131
132
|
t.equal(headersEtag, '2', 'Headers got expected etag')
|
|
132
133
|
t.equal(headersContentType, 'text/plain; charset=utf-8', 'Headers got expected mime type')
|
|
@@ -296,6 +297,24 @@ test('DELETE a directory', async (t) => {
|
|
|
296
297
|
const entries = await listDirRequest.json()
|
|
297
298
|
t.deepEqual(entries, [], 'subfolder got deleted')
|
|
298
299
|
})
|
|
300
|
+
test.only('DELETE a drive from storage', async (t) => {
|
|
301
|
+
const created = await nextURL(t)
|
|
302
|
+
|
|
303
|
+
const uploadLocation = new URL('./subfolder/example.txt', created)
|
|
304
|
+
const uploadResponse = await fetch(uploadLocation, {
|
|
305
|
+
method: 'put',
|
|
306
|
+
body: SAMPLE_CONTENT
|
|
307
|
+
})
|
|
308
|
+
await checkResponse(uploadResponse, t)
|
|
309
|
+
|
|
310
|
+
const purgeResponse = await fetch(created, { method: 'delete' })
|
|
311
|
+
|
|
312
|
+
await checkResponse(purgeResponse, t, 'Able to purge')
|
|
313
|
+
|
|
314
|
+
const listDirRequest = await fetch(created)
|
|
315
|
+
|
|
316
|
+
t.notOk(listDirRequest.ok, 'Error when trying to read after purge')
|
|
317
|
+
})
|
|
299
318
|
test('Read index.html', async (t) => {
|
|
300
319
|
const created = await nextURL(t)
|
|
301
320
|
const uploadLocation = new URL('./index.html', created)
|
|
@@ -333,6 +352,22 @@ test('Ignore index.html with noResolve', async (t) => {
|
|
|
333
352
|
const entries = await listDirRequest.json()
|
|
334
353
|
t.deepEqual(entries, ['index.html'], 'able to list index.html')
|
|
335
354
|
})
|
|
355
|
+
test('Ensure that noResolve works with file paths', async (t) => {
|
|
356
|
+
const created = await nextURL(t)
|
|
357
|
+
const uploadLocation = new URL('./example.txt', created)
|
|
358
|
+
const uploadResponse = await fetch(uploadLocation, {
|
|
359
|
+
method: 'put',
|
|
360
|
+
body: SAMPLE_CONTENT
|
|
361
|
+
})
|
|
362
|
+
await checkResponse(uploadResponse, t)
|
|
363
|
+
|
|
364
|
+
const noResolve = uploadLocation.href + '?noResolve'
|
|
365
|
+
const getRequest = await fetch(noResolve)
|
|
366
|
+
await checkResponse(getRequest, t)
|
|
367
|
+
|
|
368
|
+
const headRequest = await fetch(noResolve, { method: 'HEAD' })
|
|
369
|
+
await checkResponse(headRequest, t)
|
|
370
|
+
})
|
|
336
371
|
test('Render index.gmi', async (t) => {
|
|
337
372
|
const created = await nextURL(t)
|
|
338
373
|
const uploadLocation = new URL('./index.gmi', created)
|
|
@@ -403,14 +438,6 @@ test('Resolve pretty markdown URLs', async (t) => {
|
|
|
403
438
|
t.equal(contentType, 'text/markdown; charset=utf-8', 'Got markdown mime type')
|
|
404
439
|
})
|
|
405
440
|
|
|
406
|
-
test('Doing a `GET` on an invalid domain should cause an error', async (t) => {
|
|
407
|
-
const url = 'hyper://example/'
|
|
408
|
-
|
|
409
|
-
const failedResponse = await fetch(url)
|
|
410
|
-
|
|
411
|
-
t.notOk(failedResponse.ok, 'Response errored out due to invalid domain')
|
|
412
|
-
})
|
|
413
|
-
|
|
414
441
|
test('EventSource extension messages', async (t) => {
|
|
415
442
|
const domain = await nextURL(t)
|
|
416
443
|
|
|
@@ -474,14 +501,13 @@ test('Resolve DNS', async (t) => {
|
|
|
474
501
|
t.ok(entries.length, 'Loaded contents with some files present')
|
|
475
502
|
})
|
|
476
503
|
|
|
477
|
-
test('
|
|
478
|
-
const
|
|
504
|
+
test('Doing a `GET` on an invalid domain/public key should cause an error', async (t) => {
|
|
505
|
+
const invalidDomainResponse = await fetch('hyper://example/')
|
|
506
|
+
t.notOk(invalidDomainResponse.ok, 'Response errored out due to invalid domain')
|
|
479
507
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
t.pass('Invalid names led to an error')
|
|
484
|
-
}
|
|
508
|
+
const invalidPublicKeyResponse = await fetch('hyper://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/')
|
|
509
|
+
t.notOk(invalidPublicKeyResponse.ok, 'Response errored out due to invalid public key')
|
|
510
|
+
t.equal(invalidPublicKeyResponse.status, 404, 'Invalid public key should 404')
|
|
485
511
|
})
|
|
486
512
|
|
|
487
513
|
test('Old versions in VERSION folder', async (t) => {
|