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.
Files changed (4) hide show
  1. package/README.md +10 -0
  2. package/index.js +88 -19
  3. package/package.json +2 -2
  4. 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
- 'README.md'
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
- throw new Error(ERROR_KEY_NOT_CREATED)
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: Date.now()
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: Date.now()
433
+ mtime
394
434
  }
395
435
  })
396
436
  )
397
437
  }
398
438
  }
399
439
 
400
- return { status: 201, headers: { Location: request.url } }
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 { status: 405, body: 'Cannot PUT file to old version', headers: { Location: request.url } }
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
- return { status: 200 }
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: 200,
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: 200,
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.5.0",
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.2.3",
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('Error on invalid hostname', async (t) => {
478
- const loadResponse = await fetch('hyper://example/')
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
- if (loadResponse.ok) {
481
- throw new Error('Loading without DNS or a public key should have failed')
482
- } else {
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) => {