hypercore-fetch 9.9.1 → 10.1.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 +6 -2
- package/dist/index.d.ts +28 -0
- package/dist/test.d.ts +1 -0
- package/index.js +252 -121
- package/package.json +16 -7
- package/test.js +95 -30
- package/tsconfig.json +19 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ const fetch = await makeFetch({
|
|
|
16
16
|
writable: true
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
const someURL = `hyper://
|
|
19
|
+
const someURL = `hyper://blog.mauve.moe/`
|
|
20
20
|
|
|
21
21
|
const response = await fetch(someURL)
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ console.log(data)
|
|
|
27
27
|
|
|
28
28
|
## API
|
|
29
29
|
|
|
30
|
-
### `makeHyperFetch({sdk, writable=false, extensionMessages = writable, renderIndex}) => fetch()`
|
|
30
|
+
### `makeHyperFetch({sdk, writable=false, extensionMessages = writable, renderIndex, onLoad, onDelete}) => fetch()`
|
|
31
31
|
|
|
32
32
|
Creates a hypercore-fetch instance.
|
|
33
33
|
|
|
@@ -40,6 +40,10 @@ The `writable` flag toggles whether the `PUT`/`POST`/`DELETE` methods are availa
|
|
|
40
40
|
`renderIndex` is an optional function to override the HTML index rendering functionality. By default it will make a simple page which renders links to files and folders within the directory.
|
|
41
41
|
This function takes the `url`, `files` array and `fetch` instance as arguments.
|
|
42
42
|
|
|
43
|
+
`onLoad` is an optional function that gets called whenever a site is loaded. It gets these arguments passed in: `(url: URL, writable: boolean, key?: string)`. The `key` gets specified on creation based on the name a user assigned it. Use this to track created drives and last access times (e.g. for clearing out old drives).
|
|
44
|
+
|
|
45
|
+
`onDelete` is an optional function that gets called whenever a drive is purged from storage. Similar to `onLoad`, it gets called with a `URL` of the deleted site.
|
|
46
|
+
|
|
43
47
|
After you've created it, `fetch` will behave like it does in [browsers](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
|
44
48
|
|
|
45
49
|
### Common Headers
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('hyper-sdk').SDK} options.sdk
|
|
5
|
+
* @param {boolean} [options.writable]
|
|
6
|
+
* @param {boolean} [options.extensionMessages]
|
|
7
|
+
* @param {number} [options.timeout]
|
|
8
|
+
* @param {typeof DEFAULT_RENDER_INDEX} [options.renderIndex]
|
|
9
|
+
* @param {OnLoadHandler} [options.onLoad]
|
|
10
|
+
* @param {OnDeleteHandler} [options.onDelete]
|
|
11
|
+
* @returns {Promise<typeof globalThis.fetch>}
|
|
12
|
+
*/
|
|
13
|
+
export default function makeHyperFetch({ sdk, writable, extensionMessages, timeout, renderIndex, onLoad, onDelete }: {
|
|
14
|
+
sdk: import("hyper-sdk").SDK;
|
|
15
|
+
writable?: boolean | undefined;
|
|
16
|
+
extensionMessages?: boolean | undefined;
|
|
17
|
+
timeout?: number | undefined;
|
|
18
|
+
renderIndex?: typeof DEFAULT_RENDER_INDEX | undefined;
|
|
19
|
+
onLoad?: OnLoadHandler | undefined;
|
|
20
|
+
onDelete?: OnDeleteHandler | undefined;
|
|
21
|
+
}): Promise<typeof globalThis.fetch>;
|
|
22
|
+
export const ERROR_KEY_NOT_CREATED: "Must create key with POST before reading";
|
|
23
|
+
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";
|
|
24
|
+
export type RenderIndexHandler = (url: URL, files: string[], fetch: typeof globalThis.fetch) => Promise<string>;
|
|
25
|
+
export type OnLoadHandler = (url: URL, writable: boolean, name?: string) => void;
|
|
26
|
+
export type OnDeleteHandler = (url: URL) => void;
|
|
27
|
+
declare function DEFAULT_RENDER_INDEX(url: URL, files: string[], fetch: typeof globalThis.fetch): Promise<string>;
|
|
28
|
+
export {};
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { posix } from 'path'
|
|
2
2
|
|
|
3
|
+
// @ts-ignore
|
|
3
4
|
import { Readable, pipelinePromise } from 'streamx'
|
|
4
|
-
import Hyperdrive from 'hyperdrive'
|
|
5
5
|
import { makeRoutedFetch } from 'make-fetch'
|
|
6
6
|
import mime from 'mime/index.js'
|
|
7
7
|
import parseRange from 'range-parser'
|
|
8
8
|
import { EventIterator } from 'event-iterator'
|
|
9
9
|
|
|
10
|
+
/** @import Hypercore from 'hypercore' */
|
|
11
|
+
/** @import Hyperdrive from 'hyperdrive' */
|
|
12
|
+
|
|
13
|
+
/** @typedef {(url: URL, files: string[], fetch: typeof globalThis.fetch) => Promise<string>} RenderIndexHandler */
|
|
14
|
+
/** @typedef {(url: URL, writable: boolean, name?: string) => void} OnLoadHandler */
|
|
15
|
+
/** @typedef {(url: URL) => void} OnDeleteHandler */
|
|
16
|
+
|
|
10
17
|
const DEFAULT_TIMEOUT = 5000
|
|
11
18
|
|
|
12
19
|
const SPECIAL_DOMAIN = 'localhost'
|
|
@@ -48,6 +55,7 @@ const INDEX_FILES = [
|
|
|
48
55
|
'README.org'
|
|
49
56
|
]
|
|
50
57
|
|
|
58
|
+
/** @type {RenderIndexHandler} */
|
|
51
59
|
async function DEFAULT_RENDER_INDEX (url, files, fetch) {
|
|
52
60
|
return `
|
|
53
61
|
<!DOCTYPE html>
|
|
@@ -66,12 +74,28 @@ mime.define({
|
|
|
66
74
|
'text/gemini': ['gmi', 'gemini']
|
|
67
75
|
}, true)
|
|
68
76
|
|
|
77
|
+
function noop () {}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
*
|
|
81
|
+
* @param {object} options
|
|
82
|
+
* @param {import('hyper-sdk').SDK} options.sdk
|
|
83
|
+
* @param {boolean} [options.writable]
|
|
84
|
+
* @param {boolean} [options.extensionMessages]
|
|
85
|
+
* @param {number} [options.timeout]
|
|
86
|
+
* @param {typeof DEFAULT_RENDER_INDEX} [options.renderIndex]
|
|
87
|
+
* @param {OnLoadHandler} [options.onLoad]
|
|
88
|
+
* @param {OnDeleteHandler} [options.onDelete]
|
|
89
|
+
* @returns {Promise<typeof globalThis.fetch>}
|
|
90
|
+
*/
|
|
69
91
|
export default async function makeHyperFetch ({
|
|
70
92
|
sdk,
|
|
71
93
|
writable = false,
|
|
72
94
|
extensionMessages = writable,
|
|
73
95
|
timeout = DEFAULT_TIMEOUT,
|
|
74
|
-
renderIndex = DEFAULT_RENDER_INDEX
|
|
96
|
+
renderIndex = DEFAULT_RENDER_INDEX,
|
|
97
|
+
onLoad = noop,
|
|
98
|
+
onDelete = noop
|
|
75
99
|
}) {
|
|
76
100
|
const { fetch, router } = makeRoutedFetch({
|
|
77
101
|
onError
|
|
@@ -79,9 +103,7 @@ export default async function makeHyperFetch ({
|
|
|
79
103
|
|
|
80
104
|
// Map loaded drive hostnames to their keys
|
|
81
105
|
// TODO: Track LRU + cache clearing
|
|
82
|
-
const drives = new Map()
|
|
83
106
|
const extensions = new Map()
|
|
84
|
-
const cores = new Map()
|
|
85
107
|
|
|
86
108
|
if (extensionMessages) {
|
|
87
109
|
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`, listExtensions)
|
|
@@ -106,8 +128,14 @@ export default async function makeHyperFetch ({
|
|
|
106
128
|
router.get('hyper://*/**', getFiles)
|
|
107
129
|
router.head('hyper://*/**', headFiles)
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
/**
|
|
132
|
+
*
|
|
133
|
+
* @param {Error} e
|
|
134
|
+
* @returns {Promise<import('make-fetch').ResponseLike>}
|
|
135
|
+
*/
|
|
136
|
+
async function onError (e) {
|
|
110
137
|
return {
|
|
138
|
+
// @ts-ignore
|
|
111
139
|
status: e.statusCode || 500,
|
|
112
140
|
headers: {
|
|
113
141
|
'Content-Type': 'text/plain; charset=utf-8'
|
|
@@ -116,95 +144,12 @@ export default async function makeHyperFetch ({
|
|
|
116
144
|
}
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
cores.set(core.id, core)
|
|
126
|
-
cores.set(core.url, core)
|
|
127
|
-
return core
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function getDBCoreForName (name) {
|
|
131
|
-
const corestore = sdk.namespace(name)
|
|
132
|
-
const dbCore = corestore.get({ name: 'db' })
|
|
133
|
-
await dbCore.ready()
|
|
134
|
-
|
|
135
|
-
if (!dbCore.discovery) {
|
|
136
|
-
const discovery = sdk.join(dbCore.discoveryKey)
|
|
137
|
-
dbCore.discovery = discovery
|
|
138
|
-
dbCore.once('close', () => {
|
|
139
|
-
discovery.destroy()
|
|
140
|
-
})
|
|
141
|
-
await discovery.flushed()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return dbCore
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function getDrive (hostname, errorOnNew = false) {
|
|
148
|
-
if (drives.has(hostname)) {
|
|
149
|
-
return drives.get(hostname)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const core = await getCore(hostname)
|
|
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
|
-
|
|
161
|
-
const corestore = sdk.namespace(core.id)
|
|
162
|
-
const drive = new Hyperdrive(corestore, core.key)
|
|
163
|
-
|
|
164
|
-
await drive.ready()
|
|
165
|
-
|
|
166
|
-
drive.once('close', () => {
|
|
167
|
-
drives.delete(drive.core.id)
|
|
168
|
-
drives.delete(hostname)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
drives.set(drive.core.id, drive)
|
|
172
|
-
drives.set(drive.core.url, drive)
|
|
173
|
-
drives.set(hostname, drive)
|
|
174
|
-
|
|
175
|
-
return drive
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function getDriveFromKey (key, errorOnNew = false) {
|
|
179
|
-
if (drives.has(key)) {
|
|
180
|
-
return drives.get(key)
|
|
181
|
-
}
|
|
182
|
-
const core = await getDBCoreForName(key)
|
|
183
|
-
|
|
184
|
-
if (!core.length && errorOnNew) {
|
|
185
|
-
const e = new Error(ERROR_KEY_NOT_CREATED)
|
|
186
|
-
e.statusCode = 404
|
|
187
|
-
throw e
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const corestore = sdk.namespace(key)
|
|
191
|
-
const drive = new Hyperdrive(corestore)
|
|
192
|
-
|
|
193
|
-
await drive.ready()
|
|
194
|
-
|
|
195
|
-
drive.once('close', () => {
|
|
196
|
-
drives.delete(key)
|
|
197
|
-
drives.delete(drive.url)
|
|
198
|
-
drives.delete(drive.core.id)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
drives.set(key, drive)
|
|
202
|
-
drives.set(drive.url, drive)
|
|
203
|
-
drives.set(drive.core.id, drive)
|
|
204
|
-
|
|
205
|
-
return drive
|
|
206
|
-
}
|
|
207
|
-
|
|
147
|
+
/**
|
|
148
|
+
*
|
|
149
|
+
* @param {Hypercore} core
|
|
150
|
+
* @param {string} name
|
|
151
|
+
* @returns
|
|
152
|
+
*/
|
|
208
153
|
async function getExtension (core, name) {
|
|
209
154
|
const key = core.url + name
|
|
210
155
|
if (extensions.has(key)) {
|
|
@@ -217,7 +162,7 @@ export default async function makeHyperFetch ({
|
|
|
217
162
|
}
|
|
218
163
|
|
|
219
164
|
const extension = core.registerExtension(name, {
|
|
220
|
-
encoding: '
|
|
165
|
+
encoding: 'utf-8',
|
|
221
166
|
onmessage: (content, peer) => {
|
|
222
167
|
core.emit(EXTENSION_EVENT, name, content, peer)
|
|
223
168
|
}
|
|
@@ -228,6 +173,11 @@ export default async function makeHyperFetch ({
|
|
|
228
173
|
return extension
|
|
229
174
|
}
|
|
230
175
|
|
|
176
|
+
/**
|
|
177
|
+
* @param {Hypercore} core
|
|
178
|
+
* @param {string} name
|
|
179
|
+
* @returns
|
|
180
|
+
*/
|
|
231
181
|
async function getExtensionPeers (core, name) {
|
|
232
182
|
// List peers with this extension
|
|
233
183
|
const allPeers = core.peers
|
|
@@ -236,18 +186,32 @@ export default async function makeHyperFetch ({
|
|
|
236
186
|
})
|
|
237
187
|
}
|
|
238
188
|
|
|
189
|
+
/**
|
|
190
|
+
* @param {Hypercore} core
|
|
191
|
+
* @returns
|
|
192
|
+
*/
|
|
239
193
|
function listExtensionNames (core) {
|
|
240
194
|
return [...core.extensions.keys()]
|
|
241
195
|
}
|
|
242
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @param {Request} request
|
|
199
|
+
* @returns
|
|
200
|
+
*/
|
|
243
201
|
async function listExtensions (request) {
|
|
244
202
|
const { hostname } = new URL(request.url)
|
|
245
203
|
const accept = request.headers.get('Accept') || ''
|
|
246
204
|
|
|
247
|
-
const core = await
|
|
205
|
+
const core = await sdk.get(`hyper://${hostname}/`)
|
|
248
206
|
|
|
249
207
|
if (accept.includes('text/event-stream')) {
|
|
250
208
|
const events = new EventIterator(({ push }) => {
|
|
209
|
+
/**
|
|
210
|
+
*
|
|
211
|
+
* @param {string} name
|
|
212
|
+
* @param {string} content
|
|
213
|
+
* @param {import('hypercore').Peer} peer
|
|
214
|
+
*/
|
|
251
215
|
function onMessage (name, content, peer) {
|
|
252
216
|
const id = peer.remotePublicKey.toString('hex')
|
|
253
217
|
// TODO: Fancy verification on the `name`?
|
|
@@ -256,10 +220,18 @@ export default async function makeHyperFetch ({
|
|
|
256
220
|
|
|
257
221
|
push(`id:${id}\nevent:${name}\n${data}\n`)
|
|
258
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {import('hypercore').Peer} peer
|
|
226
|
+
*/
|
|
259
227
|
function onPeerOpen (peer) {
|
|
260
228
|
const id = peer.remotePublicKey.toString('hex')
|
|
261
229
|
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
262
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {import('hypercore').Peer} peer
|
|
234
|
+
*/
|
|
263
235
|
function onPeerRemove (peer) {
|
|
264
236
|
// Whatever, probably an uninitialized peer
|
|
265
237
|
if (!peer.remotePublicKey) return
|
|
@@ -293,12 +265,16 @@ export default async function makeHyperFetch ({
|
|
|
293
265
|
}
|
|
294
266
|
}
|
|
295
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @param {Request} request
|
|
270
|
+
* @returns
|
|
271
|
+
*/
|
|
296
272
|
async function listenExtension (request) {
|
|
297
273
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
298
274
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
299
275
|
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
300
276
|
|
|
301
|
-
const core = await
|
|
277
|
+
const core = await sdk.get(`hyper://${hostname}/`)
|
|
302
278
|
|
|
303
279
|
await getExtension(core, name)
|
|
304
280
|
|
|
@@ -315,13 +291,17 @@ export default async function makeHyperFetch ({
|
|
|
315
291
|
}
|
|
316
292
|
}
|
|
317
293
|
|
|
294
|
+
/**
|
|
295
|
+
* @param {Request} request
|
|
296
|
+
* @returns
|
|
297
|
+
*/
|
|
318
298
|
async function broadcastExtension (request) {
|
|
319
299
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
320
300
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
321
301
|
|
|
322
302
|
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
323
303
|
|
|
324
|
-
const core = await
|
|
304
|
+
const core = await sdk.get(`hyper://${hostname}/`)
|
|
325
305
|
|
|
326
306
|
const extension = await getExtension(core, name)
|
|
327
307
|
const data = await request.text()
|
|
@@ -330,6 +310,10 @@ export default async function makeHyperFetch ({
|
|
|
330
310
|
return { status: 200 }
|
|
331
311
|
}
|
|
332
312
|
|
|
313
|
+
/**
|
|
314
|
+
* @param {Request} request
|
|
315
|
+
* @returns
|
|
316
|
+
*/
|
|
333
317
|
async function extensionToPeer (request) {
|
|
334
318
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
335
319
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
@@ -337,7 +321,7 @@ export default async function makeHyperFetch ({
|
|
|
337
321
|
const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
338
322
|
const [name, extensionPeer] = subFolder.split('/')
|
|
339
323
|
|
|
340
|
-
const core = await
|
|
324
|
+
const core = await sdk.get(`hyper://${hostname}/`)
|
|
341
325
|
|
|
342
326
|
const extension = await getExtension(core, name)
|
|
343
327
|
const peers = await getExtensionPeers(core, name)
|
|
@@ -356,6 +340,10 @@ export default async function makeHyperFetch ({
|
|
|
356
340
|
return { status: 200 }
|
|
357
341
|
}
|
|
358
342
|
|
|
343
|
+
/**
|
|
344
|
+
* @param {Request} request
|
|
345
|
+
* @returns
|
|
346
|
+
*/
|
|
359
347
|
async function getKey (request) {
|
|
360
348
|
const key = new URL(request.url).searchParams.get('key')
|
|
361
349
|
if (!key) {
|
|
@@ -363,14 +351,16 @@ export default async function makeHyperFetch ({
|
|
|
363
351
|
}
|
|
364
352
|
|
|
365
353
|
try {
|
|
366
|
-
const drive = await
|
|
354
|
+
const drive = await sdk.getDrive(key)
|
|
355
|
+
onLoad(new URL('/', drive.url), drive.writable, key)
|
|
367
356
|
|
|
368
357
|
return { body: drive.url }
|
|
369
358
|
} catch (e) {
|
|
370
|
-
|
|
359
|
+
const message = /** @type Error */(e).message
|
|
360
|
+
if (message === ERROR_KEY_NOT_CREATED) {
|
|
371
361
|
return {
|
|
372
362
|
status: 400,
|
|
373
|
-
body:
|
|
363
|
+
body: message,
|
|
374
364
|
headers: {
|
|
375
365
|
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
376
366
|
}
|
|
@@ -379,6 +369,10 @@ export default async function makeHyperFetch ({
|
|
|
379
369
|
}
|
|
380
370
|
}
|
|
381
371
|
|
|
372
|
+
/**
|
|
373
|
+
* @param {Request} request
|
|
374
|
+
* @returns
|
|
375
|
+
*/
|
|
382
376
|
async function createKey (request) {
|
|
383
377
|
// TODO: Allow importing secret keys here
|
|
384
378
|
// Maybe specify a seed to use for generating the blobs?
|
|
@@ -389,29 +383,38 @@ export default async function makeHyperFetch ({
|
|
|
389
383
|
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
390
384
|
}
|
|
391
385
|
|
|
392
|
-
const drive = await
|
|
386
|
+
const drive = await sdk.getDrive(key)
|
|
387
|
+
onLoad(new URL('/', drive.url), drive.writable, key)
|
|
393
388
|
|
|
394
389
|
return { body: drive.url }
|
|
395
390
|
}
|
|
396
391
|
|
|
392
|
+
/**
|
|
393
|
+
* @param {Request} request
|
|
394
|
+
* @returns
|
|
395
|
+
*/
|
|
397
396
|
async function putFiles (request) {
|
|
398
397
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
399
398
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
400
399
|
const contentType = request.headers.get('Content-Type') || ''
|
|
401
|
-
const
|
|
400
|
+
const lastModified = request.headers.get('Last-Modified')
|
|
401
|
+
const mtime = lastModified ? Date.parse(lastModified) : Date.now()
|
|
402
402
|
const isFormData = contentType.includes('multipart/form-data')
|
|
403
403
|
|
|
404
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
404
|
+
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
405
405
|
|
|
406
|
-
if (!drive.
|
|
406
|
+
if (!drive.writable) {
|
|
407
407
|
return { status: 403, body: `Cannot PUT file to read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
onLoad(new URL('/', request.url), drive.writable)
|
|
411
|
+
|
|
410
412
|
if (isFormData) {
|
|
411
413
|
// It's a form! Get the files out and process them
|
|
412
414
|
const formData = await request.formData()
|
|
413
415
|
for (const [name, data] of formData) {
|
|
414
416
|
if (name !== 'file') continue
|
|
417
|
+
if (typeof data === 'string') continue
|
|
415
418
|
const filePath = posix.join(pathname, data.name)
|
|
416
419
|
await pipelinePromise(
|
|
417
420
|
Readable.from(data.stream()),
|
|
@@ -449,6 +452,10 @@ export default async function makeHyperFetch ({
|
|
|
449
452
|
return { status: 201, headers }
|
|
450
453
|
}
|
|
451
454
|
|
|
455
|
+
/**
|
|
456
|
+
* @param {Request} request
|
|
457
|
+
* @returns
|
|
458
|
+
*/
|
|
452
459
|
function putFilesVersioned (request) {
|
|
453
460
|
return {
|
|
454
461
|
status: 405,
|
|
@@ -457,25 +464,35 @@ export default async function makeHyperFetch ({
|
|
|
457
464
|
}
|
|
458
465
|
}
|
|
459
466
|
|
|
467
|
+
/**
|
|
468
|
+
* @param {Request} request
|
|
469
|
+
* @returns
|
|
470
|
+
*/
|
|
460
471
|
async function deleteDrive (request) {
|
|
461
472
|
const { hostname } = new URL(request.url)
|
|
462
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
473
|
+
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
474
|
+
onDelete(new URL('/', request.url))
|
|
463
475
|
|
|
464
476
|
await drive.purge()
|
|
465
477
|
|
|
466
478
|
return { status: 200 }
|
|
467
479
|
}
|
|
468
480
|
|
|
481
|
+
/**
|
|
482
|
+
* @param {Request} request
|
|
483
|
+
* @returns
|
|
484
|
+
*/
|
|
469
485
|
async function deleteFiles (request) {
|
|
470
486
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
471
487
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
472
488
|
|
|
473
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
474
|
-
|
|
475
|
-
if (!drive.db.feed.writable) {
|
|
489
|
+
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
490
|
+
if (!drive.writable) {
|
|
476
491
|
return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
477
492
|
}
|
|
478
493
|
|
|
494
|
+
onLoad(new URL('/', request.url), drive.writable)
|
|
495
|
+
|
|
479
496
|
if (pathname.endsWith('/')) {
|
|
480
497
|
let didDelete = false
|
|
481
498
|
for await (const entry of drive.list(pathname)) {
|
|
@@ -505,10 +522,18 @@ export default async function makeHyperFetch ({
|
|
|
505
522
|
return { status: 200, headers }
|
|
506
523
|
}
|
|
507
524
|
|
|
525
|
+
/**
|
|
526
|
+
* @param {Request} request
|
|
527
|
+
* @returns
|
|
528
|
+
*/
|
|
508
529
|
function deleteFilesVersioned (request) {
|
|
509
530
|
return { status: 405, body: 'Cannot DELETE old version', headers: { Location: request.url } }
|
|
510
531
|
}
|
|
511
532
|
|
|
533
|
+
/**
|
|
534
|
+
* @param {Request} request
|
|
535
|
+
* @returns
|
|
536
|
+
*/
|
|
512
537
|
async function headFilesVersioned (request) {
|
|
513
538
|
const url = new URL(request.url)
|
|
514
539
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
@@ -519,16 +544,27 @@ export default async function makeHyperFetch ({
|
|
|
519
544
|
const noResolve = searchParams.has('noResolve')
|
|
520
545
|
|
|
521
546
|
const parts = pathname.split('/')
|
|
522
|
-
const version = parts[3]
|
|
547
|
+
const version = parseInt(parts[3], 10)
|
|
523
548
|
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
524
549
|
|
|
525
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
550
|
+
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
551
|
+
|
|
552
|
+
if (!drive.writable && !drive.core.length) {
|
|
553
|
+
return {
|
|
554
|
+
status: 404,
|
|
555
|
+
body: 'Peers Not Found'
|
|
556
|
+
}
|
|
557
|
+
}
|
|
526
558
|
|
|
527
559
|
const snapshot = await drive.checkout(version)
|
|
528
560
|
|
|
529
561
|
return serveHead(snapshot, realPath, { accept, isRanged, noResolve })
|
|
530
562
|
}
|
|
531
563
|
|
|
564
|
+
/**
|
|
565
|
+
* @param {Request} request
|
|
566
|
+
* @returns
|
|
567
|
+
*/
|
|
532
568
|
async function headFiles (request) {
|
|
533
569
|
const url = new URL(request.url)
|
|
534
570
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
@@ -538,24 +574,42 @@ export default async function makeHyperFetch ({
|
|
|
538
574
|
const isRanged = request.headers.get('Range') || ''
|
|
539
575
|
const noResolve = searchParams.has('noResolve')
|
|
540
576
|
|
|
541
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
577
|
+
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
578
|
+
|
|
579
|
+
if (!drive.writable && !drive.core.length) {
|
|
580
|
+
return {
|
|
581
|
+
status: 404,
|
|
582
|
+
body: 'Peers Not Found'
|
|
583
|
+
}
|
|
584
|
+
}
|
|
542
585
|
|
|
543
586
|
return serveHead(drive, pathname, { accept, isRanged, noResolve })
|
|
544
587
|
}
|
|
545
588
|
|
|
589
|
+
/**
|
|
590
|
+
*
|
|
591
|
+
* @param {Hyperdrive} drive
|
|
592
|
+
* @param {*} pathname
|
|
593
|
+
* @param {object} options
|
|
594
|
+
* @param {string} options.accept
|
|
595
|
+
* @param {string} options.isRanged
|
|
596
|
+
* @param {boolean} options.noResolve
|
|
597
|
+
* @returns
|
|
598
|
+
*/
|
|
546
599
|
async function serveHead (drive, pathname, { accept, isRanged, noResolve }) {
|
|
547
600
|
const isDirectory = pathname.endsWith('/')
|
|
548
601
|
const fullURL = new URL(pathname, drive.url).href
|
|
549
602
|
|
|
550
|
-
const isWritable = writable && drive.
|
|
603
|
+
const isWritable = writable && drive.writable
|
|
551
604
|
|
|
552
605
|
const Allow = isWritable ? BASIC_METHODS.concat(WRITABLE_METHODS) : BASIC_METHODS
|
|
553
606
|
|
|
607
|
+
/** @type {{[key: string]: string}} */
|
|
554
608
|
const resHeaders = {
|
|
555
609
|
ETag: `${drive.version}`,
|
|
556
610
|
'Accept-Ranges': 'bytes',
|
|
557
611
|
Link: `<${fullURL}>; rel="canonical"`,
|
|
558
|
-
Allow
|
|
612
|
+
Allow: Allow.toString()
|
|
559
613
|
}
|
|
560
614
|
|
|
561
615
|
if (isDirectory) {
|
|
@@ -627,8 +681,9 @@ export default async function makeHyperFetch ({
|
|
|
627
681
|
const size = entry.value.blob.byteLength
|
|
628
682
|
if (isRanged) {
|
|
629
683
|
const ranges = parseRange(size, isRanged)
|
|
684
|
+
const isRangeValid = ranges !== -1 && ranges !== -2 && ranges
|
|
630
685
|
|
|
631
|
-
if (
|
|
686
|
+
if (isRangeValid && ranges.length && ranges.type === 'bytes') {
|
|
632
687
|
const [{ start, end }] = ranges
|
|
633
688
|
const length = (end - start + 1)
|
|
634
689
|
|
|
@@ -649,6 +704,10 @@ export default async function makeHyperFetch ({
|
|
|
649
704
|
}
|
|
650
705
|
}
|
|
651
706
|
|
|
707
|
+
/**
|
|
708
|
+
* @param {Request} request
|
|
709
|
+
* @returns
|
|
710
|
+
*/
|
|
652
711
|
async function getFilesVersioned (request) {
|
|
653
712
|
const url = new URL(request.url)
|
|
654
713
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
@@ -659,18 +718,31 @@ export default async function makeHyperFetch ({
|
|
|
659
718
|
const noResolve = searchParams.has('noResolve')
|
|
660
719
|
|
|
661
720
|
const parts = pathname.split('/')
|
|
662
|
-
const version = parts[3]
|
|
721
|
+
const version = parseInt(parts[3], 10)
|
|
663
722
|
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
664
723
|
|
|
665
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
724
|
+
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
725
|
+
|
|
726
|
+
if (!drive.writable && !drive.core.length) {
|
|
727
|
+
return {
|
|
728
|
+
status: 404,
|
|
729
|
+
body: 'Peers Not Found'
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
onLoad(new URL('/', request.url), drive.writable)
|
|
666
734
|
|
|
667
735
|
const snapshot = await drive.checkout(version)
|
|
668
736
|
|
|
669
737
|
return serveGet(snapshot, realPath, { accept, isRanged, noResolve })
|
|
670
738
|
}
|
|
671
739
|
|
|
672
|
-
|
|
740
|
+
/**
|
|
741
|
+
* @param {Request} request
|
|
742
|
+
* @returns
|
|
743
|
+
*/
|
|
673
744
|
async function getFiles (request) {
|
|
745
|
+
// TODO: Redirect on directories without trailing slash
|
|
674
746
|
const url = new URL(request.url)
|
|
675
747
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
676
748
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
@@ -679,11 +751,29 @@ export default async function makeHyperFetch ({
|
|
|
679
751
|
const isRanged = request.headers.get('Range') || ''
|
|
680
752
|
const noResolve = searchParams.has('noResolve')
|
|
681
753
|
|
|
682
|
-
const drive = await getDrive(`hyper://${hostname}
|
|
754
|
+
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
755
|
+
|
|
756
|
+
if (!drive.writable && !drive.core.length) {
|
|
757
|
+
return {
|
|
758
|
+
status: 404,
|
|
759
|
+
body: 'Peers Not Found'
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
onLoad(new URL('/', request.url), drive.writable)
|
|
683
764
|
|
|
684
765
|
return serveGet(drive, pathname, { accept, isRanged, noResolve })
|
|
685
766
|
}
|
|
686
767
|
|
|
768
|
+
/**
|
|
769
|
+
* @param {Hyperdrive} drive
|
|
770
|
+
* @param {string} pathname
|
|
771
|
+
* @param {object} options
|
|
772
|
+
* @param {string} options.accept
|
|
773
|
+
* @param {string} options.isRanged
|
|
774
|
+
* @param {boolean} options.noResolve
|
|
775
|
+
* @returns
|
|
776
|
+
*/
|
|
687
777
|
async function serveGet (drive, pathname, { accept, isRanged, noResolve }) {
|
|
688
778
|
const isDirectory = pathname.endsWith('/')
|
|
689
779
|
const fullURL = new URL(pathname, drive.url).href
|
|
@@ -749,6 +839,13 @@ export default async function makeHyperFetch ({
|
|
|
749
839
|
return fetch
|
|
750
840
|
}
|
|
751
841
|
|
|
842
|
+
/**
|
|
843
|
+
*
|
|
844
|
+
* @param {Hyperdrive} drive
|
|
845
|
+
* @param {string} pathname
|
|
846
|
+
* @param {string} [isRanged]
|
|
847
|
+
* @returns
|
|
848
|
+
*/
|
|
752
849
|
async function serveFile (drive, pathname, isRanged) {
|
|
753
850
|
const contentType = getMimeType(pathname)
|
|
754
851
|
|
|
@@ -756,6 +853,7 @@ async function serveFile (drive, pathname, isRanged) {
|
|
|
756
853
|
|
|
757
854
|
const entry = await drive.entry(pathname)
|
|
758
855
|
|
|
856
|
+
/** @type {{[key: string]: string}} */
|
|
759
857
|
const resHeaders = {
|
|
760
858
|
ETag: `${entry.seq + 1}`,
|
|
761
859
|
[HEADER_CONTENT_TYPE]: contentType,
|
|
@@ -771,8 +869,9 @@ async function serveFile (drive, pathname, isRanged) {
|
|
|
771
869
|
const size = entry.value.blob.byteLength
|
|
772
870
|
if (isRanged) {
|
|
773
871
|
const ranges = parseRange(size, isRanged)
|
|
872
|
+
const isRangeValid = ranges !== -1 && ranges !== -2 && ranges
|
|
774
873
|
|
|
775
|
-
if (
|
|
874
|
+
if (isRangeValid && ranges.length && ranges.type === 'bytes') {
|
|
776
875
|
const [{ start, end }] = ranges
|
|
777
876
|
const length = (end - start + 1)
|
|
778
877
|
|
|
@@ -800,6 +899,10 @@ async function serveFile (drive, pathname, isRanged) {
|
|
|
800
899
|
}
|
|
801
900
|
}
|
|
802
901
|
|
|
902
|
+
/**
|
|
903
|
+
* @param {string} pathname
|
|
904
|
+
* @returns {string[]}
|
|
905
|
+
*/
|
|
803
906
|
function makeToTry (pathname) {
|
|
804
907
|
return [
|
|
805
908
|
pathname,
|
|
@@ -808,6 +911,12 @@ function makeToTry (pathname) {
|
|
|
808
911
|
]
|
|
809
912
|
}
|
|
810
913
|
|
|
914
|
+
/**
|
|
915
|
+
* @param {Hyperdrive} drive
|
|
916
|
+
* @param {string} pathname
|
|
917
|
+
* @param {boolean} noResolve
|
|
918
|
+
* @returns
|
|
919
|
+
*/
|
|
811
920
|
async function resolvePath (drive, pathname, noResolve) {
|
|
812
921
|
if (noResolve) {
|
|
813
922
|
const entry = await drive.entry(pathname)
|
|
@@ -825,6 +934,12 @@ async function resolvePath (drive, pathname, noResolve) {
|
|
|
825
934
|
return { entry: null, path: null }
|
|
826
935
|
}
|
|
827
936
|
|
|
937
|
+
/**
|
|
938
|
+
*
|
|
939
|
+
* @param {Hyperdrive} drive
|
|
940
|
+
* @param {string} [pathname]
|
|
941
|
+
* @returns
|
|
942
|
+
*/
|
|
828
943
|
async function listEntries (drive, pathname = '/') {
|
|
829
944
|
const entries = []
|
|
830
945
|
for await (const path of drive.readdir(pathname)) {
|
|
@@ -839,9 +954,15 @@ async function listEntries (drive, pathname = '/') {
|
|
|
839
954
|
return entries
|
|
840
955
|
}
|
|
841
956
|
|
|
957
|
+
/**
|
|
958
|
+
*
|
|
959
|
+
* @param {import('hypercore').Peer[]} peers
|
|
960
|
+
* @returns
|
|
961
|
+
*/
|
|
842
962
|
function formatPeers (peers) {
|
|
843
963
|
return peers.map((peer) => {
|
|
844
964
|
const remotePublicKey = peer.remotePublicKey.toString('hex')
|
|
965
|
+
// @ts-ignore
|
|
845
966
|
const remoteHost = peer.stream?.rawStream?.remoteHost
|
|
846
967
|
return {
|
|
847
968
|
remotePublicKey,
|
|
@@ -850,12 +971,22 @@ function formatPeers (peers) {
|
|
|
850
971
|
})
|
|
851
972
|
}
|
|
852
973
|
|
|
974
|
+
/**
|
|
975
|
+
*
|
|
976
|
+
* @param {string} path
|
|
977
|
+
* @returns {string}
|
|
978
|
+
*/
|
|
853
979
|
function getMimeType (path) {
|
|
854
980
|
let mimeType = mime.getType(path) || 'text/plain; charset=utf-8'
|
|
855
981
|
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
|
|
856
982
|
return mimeType
|
|
857
983
|
}
|
|
858
984
|
|
|
985
|
+
/**
|
|
986
|
+
*
|
|
987
|
+
* @param {string} path
|
|
988
|
+
* @returns {string}
|
|
989
|
+
*/
|
|
859
990
|
function ensureLeadingSlash (path) {
|
|
860
991
|
return path.startsWith('/') ? path : '/' + path
|
|
861
992
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore-fetch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.1.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",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"scripts": {
|
|
8
9
|
"test": "node test",
|
|
9
|
-
"lint": "standard --fix"
|
|
10
|
+
"lint": "standard --fix && tsc --noEmit",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"preversion": "npm run lint && npm run test"
|
|
10
13
|
},
|
|
11
14
|
"repository": {
|
|
12
15
|
"type": "git",
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
},
|
|
15
18
|
"keywords": [
|
|
16
19
|
"dat",
|
|
20
|
+
"hypercore",
|
|
17
21
|
"fetch"
|
|
18
22
|
],
|
|
19
23
|
"author": "RangerMauve",
|
|
@@ -24,16 +28,21 @@
|
|
|
24
28
|
"homepage": "https://github.com/RangerMauve/hypercore-fetch#readme",
|
|
25
29
|
"dependencies": {
|
|
26
30
|
"event-iterator": "^2.0.0",
|
|
27
|
-
"
|
|
28
|
-
"make-fetch": "^3.1.1",
|
|
31
|
+
"make-fetch": "^3.2.3",
|
|
29
32
|
"mime": "^3.0.0",
|
|
30
33
|
"range-parser": "^1.2.1",
|
|
31
34
|
"streamx": "^2.13.0"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
|
-
"@rangermauve/fetch-event-source": "^
|
|
35
|
-
"
|
|
37
|
+
"@rangermauve/fetch-event-source": "^2.0.1",
|
|
38
|
+
"@tsconfig/node20": "^20.1.8",
|
|
39
|
+
"@types/mime": "^3.0.4",
|
|
40
|
+
"@types/range-parser": "^1.2.7",
|
|
41
|
+
"@types/streamx": "^2.9.5",
|
|
42
|
+
"@types/tape": "^5.8.1",
|
|
43
|
+
"hyper-sdk": "^6.2.1",
|
|
36
44
|
"standard": "^17.0.0",
|
|
37
|
-
"tape": "^5.2.2"
|
|
45
|
+
"tape": "^5.2.2",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
38
47
|
}
|
|
39
48
|
}
|
package/test.js
CHANGED
|
@@ -3,18 +3,34 @@ import * as SDK from 'hyper-sdk'
|
|
|
3
3
|
import test from 'tape'
|
|
4
4
|
import createEventSource from '@rangermauve/fetch-event-source'
|
|
5
5
|
import { once } from 'events'
|
|
6
|
+
import os from 'os'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { rm } from 'fs/promises'
|
|
6
9
|
|
|
7
10
|
import makeHyperFetch from './index.js'
|
|
8
11
|
|
|
12
|
+
/** @import {OnLoadHandler, OnDeleteHandler} from './index.js' */
|
|
13
|
+
|
|
9
14
|
const SAMPLE_CONTENT = 'Hello World'
|
|
10
15
|
const DNS_DOMAIN = 'blog.mauve.moe'
|
|
11
16
|
let count = 0
|
|
17
|
+
|
|
18
|
+
/** @type {OnLoadHandler | null} */
|
|
19
|
+
let onLoad = null
|
|
20
|
+
/** @type {OnDeleteHandler | null} */
|
|
21
|
+
let onDelete = null
|
|
22
|
+
|
|
12
23
|
function next () {
|
|
13
24
|
return count++
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
/**
|
|
28
|
+
* @param {import('tape').Test} t
|
|
29
|
+
* @param {string} [name]
|
|
30
|
+
* @returns {Promise<string>}
|
|
31
|
+
*/
|
|
32
|
+
async function nextURL (t, name = `example${next()}`) {
|
|
33
|
+
const createResponse = await fetch(`hyper://localhost/?key=${name}`, {
|
|
18
34
|
method: 'post'
|
|
19
35
|
})
|
|
20
36
|
await checkResponse(createResponse, t, 'Created new drive')
|
|
@@ -23,12 +39,17 @@ async function nextURL (t) {
|
|
|
23
39
|
return created
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
42
|
+
const tmpSuffix = Math.random().toString().slice(3, 8)
|
|
43
|
+
const tmp = join(os.tmpdir(), `hp-ftch-${tmpSuffix}`)
|
|
44
|
+
|
|
45
|
+
const sdk1 = await SDK.create({ storage: join(tmp, 'sdk1') })
|
|
46
|
+
const sdk2 = await SDK.create({ storage: join(tmp, 'sdk2') })
|
|
28
47
|
|
|
29
48
|
const fetch = await makeHyperFetch({
|
|
30
49
|
sdk: sdk1,
|
|
31
|
-
writable: true
|
|
50
|
+
writable: true,
|
|
51
|
+
onLoad: (...args) => onLoad && onLoad(...args),
|
|
52
|
+
onDelete: (...args) => onDelete && onDelete(...args)
|
|
32
53
|
})
|
|
33
54
|
|
|
34
55
|
const fetch2 = await makeHyperFetch({
|
|
@@ -36,9 +57,12 @@ const fetch2 = await makeHyperFetch({
|
|
|
36
57
|
writable: true
|
|
37
58
|
})
|
|
38
59
|
|
|
39
|
-
test.onFinish(() => {
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
test.onFinish(async () => {
|
|
61
|
+
await Promise.all([
|
|
62
|
+
sdk1.close(),
|
|
63
|
+
sdk2.close()
|
|
64
|
+
])
|
|
65
|
+
await rm(tmp, { recursive: true })
|
|
42
66
|
})
|
|
43
67
|
|
|
44
68
|
test('Quick check', async (t) => {
|
|
@@ -73,7 +97,7 @@ test('Quick check', async (t) => {
|
|
|
73
97
|
|
|
74
98
|
const content = await uploadedContentResponse.text()
|
|
75
99
|
const contentType = uploadedContentResponse.headers.get('Content-Type')
|
|
76
|
-
const contentLink = uploadedContentResponse.headers.get('Link')
|
|
100
|
+
const contentLink = uploadedContentResponse.headers.get('Link') ?? ''
|
|
77
101
|
|
|
78
102
|
t.match(contentLink, /^<hyper:\/\/[0-9a-z]{52}\/example%20.txt>; rel="canonical"$/, 'Link header includes both public key and path.')
|
|
79
103
|
t.equal(contentType, 'text/plain; charset=utf-8', 'Content got expected mime type')
|
|
@@ -89,14 +113,6 @@ test('Quick check', async (t) => {
|
|
|
89
113
|
test('GET full url for created keys', async (t) => {
|
|
90
114
|
const keyURL = `hyper://localhost/?key=example${next()}`
|
|
91
115
|
|
|
92
|
-
const nonExistingResponse = await fetch(keyURL)
|
|
93
|
-
|
|
94
|
-
t.notOk(nonExistingResponse.ok, 'response has error before key is created')
|
|
95
|
-
const errorMessage = await nonExistingResponse.text()
|
|
96
|
-
|
|
97
|
-
t.equal(nonExistingResponse.status, 400, 'Got 400 error code')
|
|
98
|
-
t.notOk(errorMessage.startsWith('hyper://'), 'did not return hyper URL')
|
|
99
|
-
|
|
100
116
|
const createResponse = await fetch(keyURL, { method: 'post' })
|
|
101
117
|
await checkResponse(createResponse, t, 'Able to create drive')
|
|
102
118
|
|
|
@@ -126,7 +142,7 @@ test('HEAD request', async (t) => {
|
|
|
126
142
|
const headersContentLength = headResponse.headers.get('Content-Length')
|
|
127
143
|
const headersAcceptRanges = headResponse.headers.get('Accept-Ranges')
|
|
128
144
|
const headersLastModified = headResponse.headers.get('Last-Modified')
|
|
129
|
-
const headersLink = headResponse.headers.get('Link')
|
|
145
|
+
const headersLink = headResponse.headers.get('Link') ?? ''
|
|
130
146
|
|
|
131
147
|
t.equal(headResponse.status, 204, 'Response had expected status')
|
|
132
148
|
// Version at which the file was added
|
|
@@ -143,7 +159,7 @@ test('PUT file', async (t) => {
|
|
|
143
159
|
|
|
144
160
|
const uploadLocation = new URL('./example.txt', created)
|
|
145
161
|
|
|
146
|
-
const fakeDate = new Date(
|
|
162
|
+
const fakeDate = new Date().toUTCString()
|
|
147
163
|
const uploadResponse = await fetch(uploadLocation, {
|
|
148
164
|
method: 'put',
|
|
149
165
|
body: SAMPLE_CONTENT,
|
|
@@ -302,7 +318,7 @@ test('DELETE a directory', async (t) => {
|
|
|
302
318
|
const entries = await listDirRequest.json()
|
|
303
319
|
t.deepEqual(entries, [], 'subfolder got deleted')
|
|
304
320
|
})
|
|
305
|
-
test('DELETE a drive from storage', async (t) => {
|
|
321
|
+
test.skip('DELETE a drive from storage', async (t) => {
|
|
306
322
|
const created = await nextURL(t)
|
|
307
323
|
|
|
308
324
|
const uploadLocation = new URL('./subfolder/example.txt', created)
|
|
@@ -463,12 +479,12 @@ test('EventSource extension messages', async (t) => {
|
|
|
463
479
|
t.deepEqual(extensionList, ['example'], 'Got expected list of extensions')
|
|
464
480
|
|
|
465
481
|
const peerResponse1 = await fetch(extensionURL)
|
|
466
|
-
const peerList1 = await peerResponse1.json()
|
|
482
|
+
const peerList1 = /** @type {object[]} */ (await peerResponse1.json())
|
|
467
483
|
|
|
468
484
|
t.equal(peerList1.length, 1, 'Got one peer for extension message on peer1')
|
|
469
485
|
|
|
470
486
|
const peerResponse2 = await fetch2(extensionURL)
|
|
471
|
-
const peerList2 = await peerResponse2.json()
|
|
487
|
+
const peerList2 = /** @type {object[]} */ (await peerResponse2.json())
|
|
472
488
|
|
|
473
489
|
t.equal(peerList2.length, 1, 'Got one peer for extension message on peer2')
|
|
474
490
|
|
|
@@ -501,11 +517,13 @@ test('EventSource extension messages', async (t) => {
|
|
|
501
517
|
test('Resolve DNS', async (t) => {
|
|
502
518
|
const loadResponse = await fetch(`hyper://${DNS_DOMAIN}/?noResolve`)
|
|
503
519
|
|
|
504
|
-
const entries = await loadResponse.json()
|
|
520
|
+
const entries = /** @type {object[]} */ (await loadResponse.json())
|
|
505
521
|
|
|
506
522
|
t.ok(entries.length, 'Loaded contents with some files present')
|
|
507
523
|
|
|
508
|
-
const
|
|
524
|
+
const linkHeader = loadResponse.headers.get('Link') || ''
|
|
525
|
+
// @ts-ignore
|
|
526
|
+
const rawLink = linkHeader.match(/<(.+)>/)[1]
|
|
509
527
|
const loadRawURLResponse = await fetch(rawLink + '?noResolve')
|
|
510
528
|
|
|
511
529
|
const rawLinkEntries = await loadRawURLResponse.json()
|
|
@@ -518,7 +536,7 @@ test('Doing a `GET` on an invalid domain/public key should cause an error', asyn
|
|
|
518
536
|
|
|
519
537
|
const invalidPublicKeyResponse = await fetch('hyper://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/')
|
|
520
538
|
t.notOk(invalidPublicKeyResponse.ok, 'Response errored out due to invalid public key')
|
|
521
|
-
t.equal(invalidPublicKeyResponse.status, 404, 'Invalid public key should
|
|
539
|
+
t.equal(invalidPublicKeyResponse.status, 404, 'Invalid public key should error')
|
|
522
540
|
})
|
|
523
541
|
|
|
524
542
|
test('Old versions in VERSION folder', async (t) => {
|
|
@@ -615,9 +633,8 @@ test('Handle empty string pathname', async (t) => {
|
|
|
615
633
|
await checkResponse(versionedGetResponse, t)
|
|
616
634
|
t.deepEqual(await versionedGetResponse.json(), ['example.txt', 'example2.txt'], 'Returns root directory prior to DELETE')
|
|
617
635
|
|
|
618
|
-
|
|
619
636
|
// DELETE
|
|
620
|
-
await checkResponse(await fetch(urlNoTrailingSlash, { method: 'DELETE' }), t, 'Able to delete root')
|
|
637
|
+
// await checkResponse(await fetch(urlNoTrailingSlash, { method: 'DELETE' }), t, 'Able to delete root')
|
|
621
638
|
})
|
|
622
639
|
|
|
623
640
|
test('Return status 403 Forbidden on attempt to modify read-only hyperdrive', async (t) => {
|
|
@@ -646,17 +663,65 @@ test('Check hyperdrive writability', async (t) => {
|
|
|
646
663
|
const readOnlyHeadersAllow = readOnlyHeadResponse.headers.get('Allow')
|
|
647
664
|
t.equal(readOnlyHeadersAllow, 'HEAD,GET', 'Expected read-only Allows header')
|
|
648
665
|
|
|
649
|
-
const
|
|
650
|
-
const writableHeadResponse = await fetch(writableRootDirectory, { method: 'HEAD' })
|
|
666
|
+
const writableHeadResponse = await fetch(created, { method: 'HEAD' })
|
|
651
667
|
await checkResponse(writableHeadResponse, t, 'Able to load HEAD')
|
|
652
668
|
const writableHeadersAllow = writableHeadResponse.headers.get('Allow')
|
|
653
669
|
t.equal(writableHeadersAllow, 'HEAD,GET,PUT,DELETE', 'Expected writable Allows header')
|
|
654
670
|
})
|
|
655
671
|
|
|
672
|
+
test.only('onLoad and onDelete handlers', async (t) => {
|
|
673
|
+
/** @type {Parameters<OnLoadHandler> | Parameters<OnDeleteHandler> | null} */
|
|
674
|
+
let args = null
|
|
675
|
+
|
|
676
|
+
onLoad = (..._args) => {
|
|
677
|
+
args = _args
|
|
678
|
+
}
|
|
679
|
+
onDelete = (..._args) => {
|
|
680
|
+
args = _args
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const created = await nextURL(t, 'example')
|
|
684
|
+
|
|
685
|
+
if (args === null) return t.fail('onLoad did not get called')
|
|
686
|
+
// @ts-ignore For some reason TS can't tell args gets set
|
|
687
|
+
t.equal(args[0].toString(), created, 'onLoad got created URL')
|
|
688
|
+
t.equal(args[1], true, 'onLoad knows created URL is writable')
|
|
689
|
+
t.equal(args[2], 'example', 'onLoad knows key for created URL')
|
|
690
|
+
|
|
691
|
+
args = null
|
|
692
|
+
|
|
693
|
+
const toFetch = `hyper://${DNS_DOMAIN}/`
|
|
694
|
+
const response = await fetch(toFetch + 'index.html')
|
|
695
|
+
await response.text()
|
|
696
|
+
|
|
697
|
+
if (args === null) return t.fail('onLoad did not get called')
|
|
698
|
+
// @ts-ignore For some reason TS can't tell args gets set
|
|
699
|
+
t.equal(args[0].toString(), toFetch, 'onLoad got loaded URL')
|
|
700
|
+
t.equal(args[1], false, 'onLoad knows loaded URL is not writable')
|
|
701
|
+
t.equal(args[2], undefined, 'onLoad lacks key for loaded domain')
|
|
702
|
+
|
|
703
|
+
args = null
|
|
704
|
+
|
|
705
|
+
await fetch(toFetch, {
|
|
706
|
+
method: 'DELETE'
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
if (args === null) return t.fail('onDelete did not get called')
|
|
710
|
+
// @ts-ignore For some reason TS can't tell args gets set
|
|
711
|
+
t.equal(args[0].toString(), toFetch, 'onDelete got URL of deleted drive')
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
*
|
|
716
|
+
* @param {Response} response
|
|
717
|
+
* @param {import('tape').Test} t
|
|
718
|
+
* @param {string} [successMessage]
|
|
719
|
+
* @returns
|
|
720
|
+
*/
|
|
656
721
|
async function checkResponse (response, t, successMessage = 'Response OK') {
|
|
657
722
|
if (!response.ok) {
|
|
658
723
|
const message = await response.text()
|
|
659
|
-
t.fail(
|
|
724
|
+
t.fail(`HTTP Error ${response.status}:\n${message}`)
|
|
660
725
|
return false
|
|
661
726
|
} else {
|
|
662
727
|
t.pass(successMessage)
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"checkJs": true,
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"emitDeclarationOnly": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
},
|
|
12
|
+
"extends": "@tsconfig/node20/tsconfig.json",
|
|
13
|
+
"files": ["./index.js","./test.js"],
|
|
14
|
+
"include": [
|
|
15
|
+
"./index.js",
|
|
16
|
+
"./test.js",
|
|
17
|
+
"./node_modules/hyper-sdk/types/*.d.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|