hypercore-fetch 9.6.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 +60 -13
  3. package/package.json +2 -2
  4. package/test.js +25 -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,7 +399,7 @@ 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 } }
@@ -419,11 +457,20 @@ export default async function makeHyperFetch ({
419
457
  }
420
458
  }
421
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 }
467
+ }
468
+
422
469
  async function deleteFiles (request) {
423
470
  const { hostname, pathname: rawPathname } = new URL(request.url)
424
471
  const pathname = decodeURI(ensureLeadingSlash(rawPathname))
425
472
 
426
- const drive = await getDrive(`hyper://${hostname}`)
473
+ const drive = await getDrive(`hyper://${hostname}/`, true)
427
474
 
428
475
  if (!drive.db.feed.writable) {
429
476
  return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
@@ -475,7 +522,7 @@ export default async function makeHyperFetch ({
475
522
  const version = parts[3]
476
523
  const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
477
524
 
478
- const drive = await getDrive(`hyper://${hostname}`)
525
+ const drive = await getDrive(`hyper://${hostname}/`, true)
479
526
 
480
527
  const snapshot = await drive.checkout(version)
481
528
 
@@ -491,7 +538,7 @@ export default async function makeHyperFetch ({
491
538
  const isRanged = request.headers.get('Range') || ''
492
539
  const noResolve = searchParams.has('noResolve')
493
540
 
494
- const drive = await getDrive(`hyper://${hostname}`)
541
+ const drive = await getDrive(`hyper://${hostname}/`, true)
495
542
 
496
543
  return serveHead(drive, pathname, { accept, isRanged, noResolve })
497
544
  }
@@ -577,7 +624,7 @@ export default async function makeHyperFetch ({
577
624
  resHeaders[HEADER_LAST_MODIFIED] = date.toUTCString()
578
625
  }
579
626
 
580
- const size = entry.value.byteLength
627
+ const size = entry.value.blob.byteLength
581
628
  if (isRanged) {
582
629
  const ranges = parseRange(size, isRanged)
583
630
 
@@ -586,7 +633,7 @@ export default async function makeHyperFetch ({
586
633
  const length = (end - start + 1)
587
634
 
588
635
  return {
589
- status: 200,
636
+ status: 204,
590
637
  headers: {
591
638
  ...resHeaders,
592
639
  'Content-Length': `${length}`,
@@ -597,7 +644,7 @@ export default async function makeHyperFetch ({
597
644
  }
598
645
 
599
646
  return {
600
- status: 200,
647
+ status: 204,
601
648
  headers: resHeaders
602
649
  }
603
650
  }
@@ -615,7 +662,7 @@ export default async function makeHyperFetch ({
615
662
  const version = parts[3]
616
663
  const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
617
664
 
618
- const drive = await getDrive(`hyper://${hostname}`)
665
+ const drive = await getDrive(`hyper://${hostname}/`, true)
619
666
 
620
667
  const snapshot = await drive.checkout(version)
621
668
 
@@ -632,7 +679,7 @@ export default async function makeHyperFetch ({
632
679
  const isRanged = request.headers.get('Range') || ''
633
680
  const noResolve = searchParams.has('noResolve')
634
681
 
635
- const drive = await getDrive(`hyper://${hostname}`)
682
+ const drive = await getDrive(`hyper://${hostname}/`, true)
636
683
 
637
684
  return serveGet(drive, pathname, { accept, isRanged, noResolve })
638
685
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypercore-fetch",
3
- "version": "9.6.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)
@@ -419,14 +438,6 @@ test('Resolve pretty markdown URLs', async (t) => {
419
438
  t.equal(contentType, 'text/markdown; charset=utf-8', 'Got markdown mime type')
420
439
  })
421
440
 
422
- test('Doing a `GET` on an invalid domain should cause an error', async (t) => {
423
- const url = 'hyper://example/'
424
-
425
- const failedResponse = await fetch(url)
426
-
427
- t.notOk(failedResponse.ok, 'Response errored out due to invalid domain')
428
- })
429
-
430
441
  test('EventSource extension messages', async (t) => {
431
442
  const domain = await nextURL(t)
432
443
 
@@ -490,14 +501,13 @@ test('Resolve DNS', async (t) => {
490
501
  t.ok(entries.length, 'Loaded contents with some files present')
491
502
  })
492
503
 
493
- test('Error on invalid hostname', async (t) => {
494
- 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')
495
507
 
496
- if (loadResponse.ok) {
497
- throw new Error('Loading without DNS or a public key should have failed')
498
- } else {
499
- t.pass('Invalid names led to an error')
500
- }
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')
501
511
  })
502
512
 
503
513
  test('Old versions in VERSION folder', async (t) => {