hypercore-fetch 10.0.0 → 10.2.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 +26 -6
- package/dist/index.d.ts +28 -0
- package/dist/test.d.ts +1 -0
- package/index.js +322 -30
- package/package.json +16 -6
- package/test.js +77 -11
- package/tsconfig.json +19 -0
package/README.md
CHANGED
|
@@ -11,23 +11,35 @@ import * as SDK from 'hyper-sdk'
|
|
|
11
11
|
// Create in-memory hyper-sdk instance
|
|
12
12
|
const sdk = await SDK.create({storage: false})
|
|
13
13
|
|
|
14
|
+
// Init
|
|
14
15
|
const fetch = await makeFetch({
|
|
15
16
|
sdk: true,
|
|
16
17
|
writable: true
|
|
17
18
|
})
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
// Download
|
|
21
|
+
const someURL = `hyper://blog.mauve.moe/`
|
|
20
22
|
|
|
21
23
|
const response = await fetch(someURL)
|
|
22
24
|
|
|
23
25
|
const data = await response.text()
|
|
24
26
|
|
|
25
27
|
console.log(data)
|
|
28
|
+
|
|
29
|
+
const response = await fetch('hyper://localhost/example.txt', {
|
|
30
|
+
method: 'PUT',
|
|
31
|
+
body: 'Hello World'
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// This is where the data got uploaded
|
|
35
|
+
const location = response.headers.get('Location')
|
|
36
|
+
// Clear the response body or else electron will flip out
|
|
37
|
+
await response.text()
|
|
26
38
|
```
|
|
27
39
|
|
|
28
40
|
## API
|
|
29
41
|
|
|
30
|
-
### `makeHyperFetch({sdk, writable=false, extensionMessages = writable, renderIndex}) => fetch()`
|
|
42
|
+
### `makeHyperFetch({sdk, writable=false, extensionMessages = writable, renderIndex, onLoad, onDelete}) => fetch()`
|
|
31
43
|
|
|
32
44
|
Creates a hypercore-fetch instance.
|
|
33
45
|
|
|
@@ -40,6 +52,10 @@ The `writable` flag toggles whether the `PUT`/`POST`/`DELETE` methods are availa
|
|
|
40
52
|
`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
53
|
This function takes the `url`, `files` array and `fetch` instance as arguments.
|
|
42
54
|
|
|
55
|
+
`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).
|
|
56
|
+
|
|
57
|
+
`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.
|
|
58
|
+
|
|
43
59
|
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
60
|
|
|
45
61
|
### Common Headers
|
|
@@ -100,6 +116,8 @@ In order to create a writable Hyperdrive with its own URL, you must first genera
|
|
|
100
116
|
|
|
101
117
|
`NAME` can be any alphanumeric string which can be used for key generation in [Corestore](https://github.com/holepunchto/corestore).
|
|
102
118
|
|
|
119
|
+
If you omit the `NAME`, it will use the name `default`.
|
|
120
|
+
|
|
103
121
|
The response body will contain a `hyper://` URL with the new Hyperdrive.
|
|
104
122
|
|
|
105
123
|
You can then use this with `PUT`/`DELETE` requests.
|
|
@@ -112,6 +130,8 @@ If you want to resolve the public key URL of a previously created Hyperdrive, yo
|
|
|
112
130
|
|
|
113
131
|
`NAME` can be any alphanumeric string which can be used for key generation in [Corestore](https://github.com/holepunchto/corestore).
|
|
114
132
|
|
|
133
|
+
If you omit the `NAME`, it will use the name `default`.
|
|
134
|
+
|
|
115
135
|
The response body will contain a `hyper://` URL with the new Hyperdrive.
|
|
116
136
|
|
|
117
137
|
You can then use this with `PUT`/`DELETE` requests.
|
|
@@ -126,7 +146,7 @@ flag.
|
|
|
126
146
|
|
|
127
147
|
The `body` can be any of the options supported by the Fetch API such as a `String`, `Blob`, `FormData`, or `ReadableStream`.
|
|
128
148
|
|
|
129
|
-
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive
|
|
149
|
+
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive, `localhost` for the `default` drive, or a domain to parse with the [DNSLink](https://www.dnslink.io/) standard.
|
|
130
150
|
|
|
131
151
|
The mtime metadata is automatically set to the current time when
|
|
132
152
|
uploading. To override this value, pass a `Last-Modified` header with a value
|
|
@@ -145,7 +165,7 @@ The `filename` will be the filename within the directory that gets created.
|
|
|
145
165
|
|
|
146
166
|
Note that you must use the name `file` for uploaded files.
|
|
147
167
|
|
|
148
|
-
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive
|
|
168
|
+
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive, `localhost` for the `default` drive, or a domain to parse with the [DNSLink](https://www.dnslink.io/) standard.
|
|
149
169
|
|
|
150
170
|
### `fetch('hyper://NAME/', {method: 'DELETE'})`
|
|
151
171
|
|
|
@@ -155,13 +175,13 @@ If this is a writable drive, your data will get fully clearned and trying to wri
|
|
|
155
175
|
|
|
156
176
|
If you try to load this drive again data will be loaded from scratch.
|
|
157
177
|
|
|
158
|
-
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive
|
|
178
|
+
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive, `localhost` for the `default` drive, or a domain to parse with the [DNSLink](https://www.dnslink.io/) standard.
|
|
159
179
|
|
|
160
180
|
### `fetch('hyper://NAME/example.txt', {method: 'DELETE'})`
|
|
161
181
|
|
|
162
182
|
You can delete a file or directory tree in a Hyperdrive by using the `DELETE` method.
|
|
163
183
|
|
|
164
|
-
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive
|
|
184
|
+
`NAME` can either be the 52 character [z32 encoded](https://github.com/mafintosh/z32) key for a Hyperdrive, `localhost` for the `default` drive, or a domain to parse with the [DNSLink](https://www.dnslink.io/) standard.
|
|
165
185
|
|
|
166
186
|
Note that this is only available with the `writable: true` flag.
|
|
167
187
|
|
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,21 @@
|
|
|
1
1
|
import { posix } from 'path'
|
|
2
2
|
|
|
3
|
+
// @ts-ignore
|
|
3
4
|
import { Readable, pipelinePromise } from 'streamx'
|
|
4
5
|
import { makeRoutedFetch } from 'make-fetch'
|
|
5
6
|
import mime from 'mime/index.js'
|
|
6
7
|
import parseRange from 'range-parser'
|
|
7
8
|
import { EventIterator } from 'event-iterator'
|
|
8
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
|
+
|
|
9
17
|
const DEFAULT_TIMEOUT = 5000
|
|
18
|
+
const DEFAULT_DRIVE = 'default'
|
|
10
19
|
|
|
11
20
|
const SPECIAL_DOMAIN = 'localhost'
|
|
12
21
|
const SPECIAL_FOLDER = '$'
|
|
@@ -47,6 +56,7 @@ const INDEX_FILES = [
|
|
|
47
56
|
'README.org'
|
|
48
57
|
]
|
|
49
58
|
|
|
59
|
+
/** @type {RenderIndexHandler} */
|
|
50
60
|
async function DEFAULT_RENDER_INDEX (url, files, fetch) {
|
|
51
61
|
return `
|
|
52
62
|
<!DOCTYPE html>
|
|
@@ -65,12 +75,30 @@ mime.define({
|
|
|
65
75
|
'text/gemini': ['gmi', 'gemini']
|
|
66
76
|
}, true)
|
|
67
77
|
|
|
78
|
+
function noop () {}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
*
|
|
82
|
+
* @param {object} options
|
|
83
|
+
* @param {import('hyper-sdk').SDK} options.sdk
|
|
84
|
+
* @param {boolean} [options.writable]
|
|
85
|
+
* @param {boolean} [options.extensionMessages]
|
|
86
|
+
* @param {number} [options.timeout]
|
|
87
|
+
* @param {string} [options.defaultDrive]
|
|
88
|
+
* @param {typeof DEFAULT_RENDER_INDEX} [options.renderIndex]
|
|
89
|
+
* @param {OnLoadHandler} [options.onLoad]
|
|
90
|
+
* @param {OnDeleteHandler} [options.onDelete]
|
|
91
|
+
* @returns {Promise<typeof globalThis.fetch>}
|
|
92
|
+
*/
|
|
68
93
|
export default async function makeHyperFetch ({
|
|
69
94
|
sdk,
|
|
70
95
|
writable = false,
|
|
71
96
|
extensionMessages = writable,
|
|
72
97
|
timeout = DEFAULT_TIMEOUT,
|
|
73
|
-
|
|
98
|
+
defaultDrive = DEFAULT_DRIVE,
|
|
99
|
+
renderIndex = DEFAULT_RENDER_INDEX,
|
|
100
|
+
onLoad = noop,
|
|
101
|
+
onDelete = noop
|
|
74
102
|
}) {
|
|
75
103
|
const { fetch, router } = makeRoutedFetch({
|
|
76
104
|
onError
|
|
@@ -90,6 +118,10 @@ export default async function makeHyperFetch ({
|
|
|
90
118
|
if (writable) {
|
|
91
119
|
router.get(`hyper://${SPECIAL_DOMAIN}/`, getKey)
|
|
92
120
|
router.post(`hyper://${SPECIAL_DOMAIN}/`, createKey)
|
|
121
|
+
router.put(`hyper://${SPECIAL_DOMAIN}/`, putFilesDefault)
|
|
122
|
+
router.delete('hyper://SPECIAL_DOMAIN/**', deleteFilesDefault)
|
|
123
|
+
router.get(`hyper://${SPECIAL_DOMAIN}/**`, getFilesDefault)
|
|
124
|
+
router.head(`hyper://${SPECIAL_DOMAIN}/**`, headFilesDefault)
|
|
93
125
|
|
|
94
126
|
router.put(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, putFilesVersioned)
|
|
95
127
|
router.put('hyper://*/**', putFiles)
|
|
@@ -103,8 +135,14 @@ export default async function makeHyperFetch ({
|
|
|
103
135
|
router.get('hyper://*/**', getFiles)
|
|
104
136
|
router.head('hyper://*/**', headFiles)
|
|
105
137
|
|
|
106
|
-
|
|
138
|
+
/**
|
|
139
|
+
*
|
|
140
|
+
* @param {Error} e
|
|
141
|
+
* @returns {Promise<import('make-fetch').ResponseLike>}
|
|
142
|
+
*/
|
|
143
|
+
async function onError (e) {
|
|
107
144
|
return {
|
|
145
|
+
// @ts-ignore
|
|
108
146
|
status: e.statusCode || 500,
|
|
109
147
|
headers: {
|
|
110
148
|
'Content-Type': 'text/plain; charset=utf-8'
|
|
@@ -113,6 +151,12 @@ export default async function makeHyperFetch ({
|
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
153
|
|
|
154
|
+
/**
|
|
155
|
+
*
|
|
156
|
+
* @param {Hypercore} core
|
|
157
|
+
* @param {string} name
|
|
158
|
+
* @returns
|
|
159
|
+
*/
|
|
116
160
|
async function getExtension (core, name) {
|
|
117
161
|
const key = core.url + name
|
|
118
162
|
if (extensions.has(key)) {
|
|
@@ -125,7 +169,7 @@ export default async function makeHyperFetch ({
|
|
|
125
169
|
}
|
|
126
170
|
|
|
127
171
|
const extension = core.registerExtension(name, {
|
|
128
|
-
encoding: '
|
|
172
|
+
encoding: 'utf-8',
|
|
129
173
|
onmessage: (content, peer) => {
|
|
130
174
|
core.emit(EXTENSION_EVENT, name, content, peer)
|
|
131
175
|
}
|
|
@@ -136,6 +180,11 @@ export default async function makeHyperFetch ({
|
|
|
136
180
|
return extension
|
|
137
181
|
}
|
|
138
182
|
|
|
183
|
+
/**
|
|
184
|
+
* @param {Hypercore} core
|
|
185
|
+
* @param {string} name
|
|
186
|
+
* @returns
|
|
187
|
+
*/
|
|
139
188
|
async function getExtensionPeers (core, name) {
|
|
140
189
|
// List peers with this extension
|
|
141
190
|
const allPeers = core.peers
|
|
@@ -144,10 +193,18 @@ export default async function makeHyperFetch ({
|
|
|
144
193
|
})
|
|
145
194
|
}
|
|
146
195
|
|
|
196
|
+
/**
|
|
197
|
+
* @param {Hypercore} core
|
|
198
|
+
* @returns
|
|
199
|
+
*/
|
|
147
200
|
function listExtensionNames (core) {
|
|
148
201
|
return [...core.extensions.keys()]
|
|
149
202
|
}
|
|
150
203
|
|
|
204
|
+
/**
|
|
205
|
+
* @param {Request} request
|
|
206
|
+
* @returns
|
|
207
|
+
*/
|
|
151
208
|
async function listExtensions (request) {
|
|
152
209
|
const { hostname } = new URL(request.url)
|
|
153
210
|
const accept = request.headers.get('Accept') || ''
|
|
@@ -156,6 +213,12 @@ export default async function makeHyperFetch ({
|
|
|
156
213
|
|
|
157
214
|
if (accept.includes('text/event-stream')) {
|
|
158
215
|
const events = new EventIterator(({ push }) => {
|
|
216
|
+
/**
|
|
217
|
+
*
|
|
218
|
+
* @param {string} name
|
|
219
|
+
* @param {string} content
|
|
220
|
+
* @param {import('hypercore').Peer} peer
|
|
221
|
+
*/
|
|
159
222
|
function onMessage (name, content, peer) {
|
|
160
223
|
const id = peer.remotePublicKey.toString('hex')
|
|
161
224
|
// TODO: Fancy verification on the `name`?
|
|
@@ -164,10 +227,18 @@ export default async function makeHyperFetch ({
|
|
|
164
227
|
|
|
165
228
|
push(`id:${id}\nevent:${name}\n${data}\n`)
|
|
166
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {import('hypercore').Peer} peer
|
|
233
|
+
*/
|
|
167
234
|
function onPeerOpen (peer) {
|
|
168
235
|
const id = peer.remotePublicKey.toString('hex')
|
|
169
236
|
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
170
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {import('hypercore').Peer} peer
|
|
241
|
+
*/
|
|
171
242
|
function onPeerRemove (peer) {
|
|
172
243
|
// Whatever, probably an uninitialized peer
|
|
173
244
|
if (!peer.remotePublicKey) return
|
|
@@ -201,6 +272,10 @@ export default async function makeHyperFetch ({
|
|
|
201
272
|
}
|
|
202
273
|
}
|
|
203
274
|
|
|
275
|
+
/**
|
|
276
|
+
* @param {Request} request
|
|
277
|
+
* @returns
|
|
278
|
+
*/
|
|
204
279
|
async function listenExtension (request) {
|
|
205
280
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
206
281
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
@@ -223,6 +298,10 @@ export default async function makeHyperFetch ({
|
|
|
223
298
|
}
|
|
224
299
|
}
|
|
225
300
|
|
|
301
|
+
/**
|
|
302
|
+
* @param {Request} request
|
|
303
|
+
* @returns
|
|
304
|
+
*/
|
|
226
305
|
async function broadcastExtension (request) {
|
|
227
306
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
228
307
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
@@ -238,6 +317,10 @@ export default async function makeHyperFetch ({
|
|
|
238
317
|
return { status: 200 }
|
|
239
318
|
}
|
|
240
319
|
|
|
320
|
+
/**
|
|
321
|
+
* @param {Request} request
|
|
322
|
+
* @returns
|
|
323
|
+
*/
|
|
241
324
|
async function extensionToPeer (request) {
|
|
242
325
|
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
243
326
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
@@ -264,21 +347,24 @@ export default async function makeHyperFetch ({
|
|
|
264
347
|
return { status: 200 }
|
|
265
348
|
}
|
|
266
349
|
|
|
350
|
+
/**
|
|
351
|
+
* @param {Request} request
|
|
352
|
+
* @returns
|
|
353
|
+
*/
|
|
267
354
|
async function getKey (request) {
|
|
268
|
-
const key = new URL(request.url).searchParams.get('key')
|
|
269
|
-
if (!key) {
|
|
270
|
-
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
271
|
-
}
|
|
355
|
+
const key = new URL(request.url).searchParams.get('key') || defaultDrive
|
|
272
356
|
|
|
273
357
|
try {
|
|
274
358
|
const drive = await sdk.getDrive(key)
|
|
359
|
+
onLoad(new URL('/', drive.url), drive.writable, key)
|
|
275
360
|
|
|
276
361
|
return { body: drive.url }
|
|
277
362
|
} catch (e) {
|
|
278
|
-
|
|
363
|
+
const message = /** @type Error */(e).message
|
|
364
|
+
if (message === ERROR_KEY_NOT_CREATED) {
|
|
279
365
|
return {
|
|
280
366
|
status: 400,
|
|
281
|
-
body:
|
|
367
|
+
body: message,
|
|
282
368
|
headers: {
|
|
283
369
|
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
284
370
|
}
|
|
@@ -287,39 +373,67 @@ export default async function makeHyperFetch ({
|
|
|
287
373
|
}
|
|
288
374
|
}
|
|
289
375
|
|
|
376
|
+
/**
|
|
377
|
+
* @param {Request} request
|
|
378
|
+
* @returns
|
|
379
|
+
*/
|
|
290
380
|
async function createKey (request) {
|
|
291
381
|
// TODO: Allow importing secret keys here
|
|
292
382
|
// Maybe specify a seed to use for generating the blobs?
|
|
293
383
|
// Else we'd need to specify the blobs keys and metadata keys
|
|
294
384
|
|
|
295
|
-
const key = new URL(request.url).searchParams.get('key')
|
|
296
|
-
if (!key) {
|
|
297
|
-
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
298
|
-
}
|
|
385
|
+
const key = new URL(request.url).searchParams.get('key') || defaultDrive
|
|
299
386
|
|
|
300
387
|
const drive = await sdk.getDrive(key)
|
|
388
|
+
onLoad(new URL('/', drive.url), drive.writable, key)
|
|
301
389
|
|
|
302
390
|
return { body: drive.url }
|
|
303
391
|
}
|
|
304
392
|
|
|
393
|
+
/**
|
|
394
|
+
* @param {Request} request
|
|
395
|
+
*/
|
|
396
|
+
async function putFilesDefault (request) {
|
|
397
|
+
const finalURL = await resolveInDefault(request.url)
|
|
398
|
+
|
|
399
|
+
return putTo(finalURL, request)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {Request} request
|
|
404
|
+
* @returns
|
|
405
|
+
*/
|
|
305
406
|
async function putFiles (request) {
|
|
306
|
-
|
|
407
|
+
return putTo(request.url, request)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @param {string} url
|
|
412
|
+
* @param {Request} request
|
|
413
|
+
* @returns
|
|
414
|
+
*/
|
|
415
|
+
async function putTo (url, request) {
|
|
416
|
+
const { hostname, pathname: rawPathname } = new URL(url)
|
|
307
417
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
308
418
|
const contentType = request.headers.get('Content-Type') || ''
|
|
309
|
-
const
|
|
419
|
+
const lastModified = request.headers.get('Last-Modified')
|
|
420
|
+
const mtime = lastModified ? Date.parse(lastModified) : Date.now()
|
|
310
421
|
const isFormData = contentType.includes('multipart/form-data')
|
|
311
422
|
|
|
312
423
|
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
313
424
|
|
|
314
|
-
if (!drive.
|
|
425
|
+
if (!drive.writable) {
|
|
315
426
|
return { status: 403, body: `Cannot PUT file to read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
316
427
|
}
|
|
317
428
|
|
|
429
|
+
onLoad(new URL('/', url), drive.writable)
|
|
430
|
+
|
|
318
431
|
if (isFormData) {
|
|
319
432
|
// It's a form! Get the files out and process them
|
|
320
433
|
const formData = await request.formData()
|
|
321
434
|
for (const [name, data] of formData) {
|
|
322
435
|
if (name !== 'file') continue
|
|
436
|
+
if (typeof data === 'string') continue
|
|
323
437
|
const filePath = posix.join(pathname, data.name)
|
|
324
438
|
await pipelinePromise(
|
|
325
439
|
Readable.from(data.stream()),
|
|
@@ -332,7 +446,7 @@ export default async function makeHyperFetch ({
|
|
|
332
446
|
}
|
|
333
447
|
} else {
|
|
334
448
|
if (pathname.endsWith('/')) {
|
|
335
|
-
return { status: 405, body: 'Cannot PUT file with trailing slash', headers: { Location:
|
|
449
|
+
return { status: 405, body: 'Cannot PUT file with trailing slash', headers: { Location: url } }
|
|
336
450
|
} else {
|
|
337
451
|
await pipelinePromise(
|
|
338
452
|
Readable.from(request.body),
|
|
@@ -357,6 +471,10 @@ export default async function makeHyperFetch ({
|
|
|
357
471
|
return { status: 201, headers }
|
|
358
472
|
}
|
|
359
473
|
|
|
474
|
+
/**
|
|
475
|
+
* @param {Request} request
|
|
476
|
+
* @returns
|
|
477
|
+
*/
|
|
360
478
|
function putFilesVersioned (request) {
|
|
361
479
|
return {
|
|
362
480
|
status: 405,
|
|
@@ -365,25 +483,54 @@ export default async function makeHyperFetch ({
|
|
|
365
483
|
}
|
|
366
484
|
}
|
|
367
485
|
|
|
486
|
+
/**
|
|
487
|
+
* @param {Request} request
|
|
488
|
+
* @returns
|
|
489
|
+
*/
|
|
368
490
|
async function deleteDrive (request) {
|
|
369
491
|
const { hostname } = new URL(request.url)
|
|
370
492
|
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
493
|
+
onDelete(new URL('/', request.url))
|
|
371
494
|
|
|
372
495
|
await drive.purge()
|
|
373
496
|
|
|
374
497
|
return { status: 200 }
|
|
375
498
|
}
|
|
376
499
|
|
|
500
|
+
/**
|
|
501
|
+
* @param {Request} request
|
|
502
|
+
* @returns
|
|
503
|
+
*/
|
|
504
|
+
async function deleteFilesDefault (request) {
|
|
505
|
+
const finalURL = await resolveInDefault(request.url)
|
|
506
|
+
|
|
507
|
+
return deleteFilesTo(finalURL, request)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* @param {Request} request
|
|
512
|
+
* @returns
|
|
513
|
+
*/
|
|
377
514
|
async function deleteFiles (request) {
|
|
378
|
-
|
|
515
|
+
return deleteFilesTo(request.url, request)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @param {string} url
|
|
520
|
+
* @param {Request} request
|
|
521
|
+
* @returns
|
|
522
|
+
*/
|
|
523
|
+
async function deleteFilesTo (url, request) {
|
|
524
|
+
const { hostname, pathname: rawPathname } = new URL(url)
|
|
379
525
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
380
526
|
|
|
381
527
|
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: request.url } }
|
|
528
|
+
if (!drive.writable) {
|
|
529
|
+
return { status: 403, body: `Cannot DELETE file in read-only drive: ${drive.url}`, headers: { Location: url } }
|
|
385
530
|
}
|
|
386
531
|
|
|
532
|
+
onLoad(new URL('/', url), drive.writable)
|
|
533
|
+
|
|
387
534
|
if (pathname.endsWith('/')) {
|
|
388
535
|
let didDelete = false
|
|
389
536
|
for await (const entry of drive.list(pathname)) {
|
|
@@ -413,10 +560,18 @@ export default async function makeHyperFetch ({
|
|
|
413
560
|
return { status: 200, headers }
|
|
414
561
|
}
|
|
415
562
|
|
|
563
|
+
/**
|
|
564
|
+
* @param {Request} request
|
|
565
|
+
* @returns
|
|
566
|
+
*/
|
|
416
567
|
function deleteFilesVersioned (request) {
|
|
417
568
|
return { status: 405, body: 'Cannot DELETE old version', headers: { Location: request.url } }
|
|
418
569
|
}
|
|
419
570
|
|
|
571
|
+
/**
|
|
572
|
+
* @param {Request} request
|
|
573
|
+
* @returns
|
|
574
|
+
*/
|
|
420
575
|
async function headFilesVersioned (request) {
|
|
421
576
|
const url = new URL(request.url)
|
|
422
577
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
@@ -427,7 +582,7 @@ export default async function makeHyperFetch ({
|
|
|
427
582
|
const noResolve = searchParams.has('noResolve')
|
|
428
583
|
|
|
429
584
|
const parts = pathname.split('/')
|
|
430
|
-
const version = parts[3]
|
|
585
|
+
const version = parseInt(parts[3], 10)
|
|
431
586
|
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
432
587
|
|
|
433
588
|
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
@@ -444,8 +599,31 @@ export default async function makeHyperFetch ({
|
|
|
444
599
|
return serveHead(snapshot, realPath, { accept, isRanged, noResolve })
|
|
445
600
|
}
|
|
446
601
|
|
|
602
|
+
/**
|
|
603
|
+
* @param {Request} request
|
|
604
|
+
* @returns
|
|
605
|
+
*/
|
|
606
|
+
async function headFilesDefault (request) {
|
|
607
|
+
const finalURL = await resolveInDefault(request.url)
|
|
608
|
+
|
|
609
|
+
return headFilesFrom(finalURL, request)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* @param {Request} request
|
|
614
|
+
* @returns
|
|
615
|
+
*/
|
|
447
616
|
async function headFiles (request) {
|
|
448
|
-
|
|
617
|
+
return headFilesFrom(request.url, request)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* @param {string} _url
|
|
622
|
+
* @param {Request} request
|
|
623
|
+
* @returns
|
|
624
|
+
*/
|
|
625
|
+
async function headFilesFrom (_url, request) {
|
|
626
|
+
const url = new URL(_url)
|
|
449
627
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
450
628
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
451
629
|
|
|
@@ -465,19 +643,30 @@ export default async function makeHyperFetch ({
|
|
|
465
643
|
return serveHead(drive, pathname, { accept, isRanged, noResolve })
|
|
466
644
|
}
|
|
467
645
|
|
|
646
|
+
/**
|
|
647
|
+
*
|
|
648
|
+
* @param {Hyperdrive} drive
|
|
649
|
+
* @param {*} pathname
|
|
650
|
+
* @param {object} options
|
|
651
|
+
* @param {string} options.accept
|
|
652
|
+
* @param {string} options.isRanged
|
|
653
|
+
* @param {boolean} options.noResolve
|
|
654
|
+
* @returns
|
|
655
|
+
*/
|
|
468
656
|
async function serveHead (drive, pathname, { accept, isRanged, noResolve }) {
|
|
469
657
|
const isDirectory = pathname.endsWith('/')
|
|
470
658
|
const fullURL = new URL(pathname, drive.url).href
|
|
471
659
|
|
|
472
|
-
const isWritable = writable && drive.
|
|
660
|
+
const isWritable = writable && drive.writable
|
|
473
661
|
|
|
474
662
|
const Allow = isWritable ? BASIC_METHODS.concat(WRITABLE_METHODS) : BASIC_METHODS
|
|
475
663
|
|
|
664
|
+
/** @type {{[key: string]: string}} */
|
|
476
665
|
const resHeaders = {
|
|
477
666
|
ETag: `${drive.version}`,
|
|
478
667
|
'Accept-Ranges': 'bytes',
|
|
479
668
|
Link: `<${fullURL}>; rel="canonical"`,
|
|
480
|
-
Allow
|
|
669
|
+
Allow: Allow.toString()
|
|
481
670
|
}
|
|
482
671
|
|
|
483
672
|
if (isDirectory) {
|
|
@@ -549,8 +738,9 @@ export default async function makeHyperFetch ({
|
|
|
549
738
|
const size = entry.value.blob.byteLength
|
|
550
739
|
if (isRanged) {
|
|
551
740
|
const ranges = parseRange(size, isRanged)
|
|
741
|
+
const isRangeValid = ranges !== -1 && ranges !== -2 && ranges
|
|
552
742
|
|
|
553
|
-
if (
|
|
743
|
+
if (isRangeValid && ranges.length && ranges.type === 'bytes') {
|
|
554
744
|
const [{ start, end }] = ranges
|
|
555
745
|
const length = (end - start + 1)
|
|
556
746
|
|
|
@@ -571,6 +761,10 @@ export default async function makeHyperFetch ({
|
|
|
571
761
|
}
|
|
572
762
|
}
|
|
573
763
|
|
|
764
|
+
/**
|
|
765
|
+
* @param {Request} request
|
|
766
|
+
* @returns
|
|
767
|
+
*/
|
|
574
768
|
async function getFilesVersioned (request) {
|
|
575
769
|
const url = new URL(request.url)
|
|
576
770
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
@@ -581,19 +775,51 @@ export default async function makeHyperFetch ({
|
|
|
581
775
|
const noResolve = searchParams.has('noResolve')
|
|
582
776
|
|
|
583
777
|
const parts = pathname.split('/')
|
|
584
|
-
const version = parts[3]
|
|
778
|
+
const version = parseInt(parts[3], 10)
|
|
585
779
|
const realPath = ensureLeadingSlash(parts.slice(4).join('/'))
|
|
586
780
|
|
|
587
781
|
const drive = await sdk.getDrive(`hyper://${hostname}/`)
|
|
588
782
|
|
|
783
|
+
if (!drive.writable && !drive.core.length) {
|
|
784
|
+
return {
|
|
785
|
+
status: 404,
|
|
786
|
+
body: 'Peers Not Found'
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
onLoad(new URL('/', request.url), drive.writable)
|
|
791
|
+
|
|
589
792
|
const snapshot = await drive.checkout(version)
|
|
590
793
|
|
|
591
794
|
return serveGet(snapshot, realPath, { accept, isRanged, noResolve })
|
|
592
795
|
}
|
|
593
796
|
|
|
594
|
-
|
|
797
|
+
/**
|
|
798
|
+
* @param {Request} request
|
|
799
|
+
* @returns
|
|
800
|
+
*/
|
|
801
|
+
async function getFilesDefault (request) {
|
|
802
|
+
const finalURL = await resolveInDefault(request.url)
|
|
803
|
+
|
|
804
|
+
return getFilesFrom(finalURL, request)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* @param {Request} request
|
|
809
|
+
* @returns
|
|
810
|
+
*/
|
|
595
811
|
async function getFiles (request) {
|
|
596
|
-
|
|
812
|
+
return getFilesFrom(request.url, request)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* @param {string} _url
|
|
817
|
+
* @param {Request} request
|
|
818
|
+
* @returns
|
|
819
|
+
*/
|
|
820
|
+
async function getFilesFrom (_url, request) {
|
|
821
|
+
// TODO: Redirect on directories without trailing slash
|
|
822
|
+
const url = new URL(_url)
|
|
597
823
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
598
824
|
const pathname = decodeURI(ensureLeadingSlash(rawPathname))
|
|
599
825
|
|
|
@@ -610,9 +836,20 @@ export default async function makeHyperFetch ({
|
|
|
610
836
|
}
|
|
611
837
|
}
|
|
612
838
|
|
|
839
|
+
onLoad(new URL('/', url), drive.writable)
|
|
840
|
+
|
|
613
841
|
return serveGet(drive, pathname, { accept, isRanged, noResolve })
|
|
614
842
|
}
|
|
615
843
|
|
|
844
|
+
/**
|
|
845
|
+
* @param {Hyperdrive} drive
|
|
846
|
+
* @param {string} pathname
|
|
847
|
+
* @param {object} options
|
|
848
|
+
* @param {string} options.accept
|
|
849
|
+
* @param {string} options.isRanged
|
|
850
|
+
* @param {boolean} options.noResolve
|
|
851
|
+
* @returns
|
|
852
|
+
*/
|
|
616
853
|
async function serveGet (drive, pathname, { accept, isRanged, noResolve }) {
|
|
617
854
|
const isDirectory = pathname.endsWith('/')
|
|
618
855
|
const fullURL = new URL(pathname, drive.url).href
|
|
@@ -675,9 +912,30 @@ export default async function makeHyperFetch ({
|
|
|
675
912
|
return serveFile(drive, path, isRanged)
|
|
676
913
|
}
|
|
677
914
|
|
|
915
|
+
/**
|
|
916
|
+
* Resolve a URL with a pathname to be within the default drive
|
|
917
|
+
* @param {string} url
|
|
918
|
+
* @returns {Promise<string>}
|
|
919
|
+
*/
|
|
920
|
+
async function resolveInDefault (url) {
|
|
921
|
+
const { pathname } = new URL(url)
|
|
922
|
+
|
|
923
|
+
const drive = await sdk.getDrive(defaultDrive)
|
|
924
|
+
const finalURL = new URL(pathname, drive.url).href
|
|
925
|
+
|
|
926
|
+
return finalURL
|
|
927
|
+
}
|
|
928
|
+
|
|
678
929
|
return fetch
|
|
679
930
|
}
|
|
680
931
|
|
|
932
|
+
/**
|
|
933
|
+
*
|
|
934
|
+
* @param {Hyperdrive} drive
|
|
935
|
+
* @param {string} pathname
|
|
936
|
+
* @param {string} [isRanged]
|
|
937
|
+
* @returns
|
|
938
|
+
*/
|
|
681
939
|
async function serveFile (drive, pathname, isRanged) {
|
|
682
940
|
const contentType = getMimeType(pathname)
|
|
683
941
|
|
|
@@ -685,6 +943,7 @@ async function serveFile (drive, pathname, isRanged) {
|
|
|
685
943
|
|
|
686
944
|
const entry = await drive.entry(pathname)
|
|
687
945
|
|
|
946
|
+
/** @type {{[key: string]: string}} */
|
|
688
947
|
const resHeaders = {
|
|
689
948
|
ETag: `${entry.seq + 1}`,
|
|
690
949
|
[HEADER_CONTENT_TYPE]: contentType,
|
|
@@ -700,8 +959,9 @@ async function serveFile (drive, pathname, isRanged) {
|
|
|
700
959
|
const size = entry.value.blob.byteLength
|
|
701
960
|
if (isRanged) {
|
|
702
961
|
const ranges = parseRange(size, isRanged)
|
|
962
|
+
const isRangeValid = ranges !== -1 && ranges !== -2 && ranges
|
|
703
963
|
|
|
704
|
-
if (
|
|
964
|
+
if (isRangeValid && ranges.length && ranges.type === 'bytes') {
|
|
705
965
|
const [{ start, end }] = ranges
|
|
706
966
|
const length = (end - start + 1)
|
|
707
967
|
|
|
@@ -729,6 +989,10 @@ async function serveFile (drive, pathname, isRanged) {
|
|
|
729
989
|
}
|
|
730
990
|
}
|
|
731
991
|
|
|
992
|
+
/**
|
|
993
|
+
* @param {string} pathname
|
|
994
|
+
* @returns {string[]}
|
|
995
|
+
*/
|
|
732
996
|
function makeToTry (pathname) {
|
|
733
997
|
return [
|
|
734
998
|
pathname,
|
|
@@ -737,6 +1001,12 @@ function makeToTry (pathname) {
|
|
|
737
1001
|
]
|
|
738
1002
|
}
|
|
739
1003
|
|
|
1004
|
+
/**
|
|
1005
|
+
* @param {Hyperdrive} drive
|
|
1006
|
+
* @param {string} pathname
|
|
1007
|
+
* @param {boolean} noResolve
|
|
1008
|
+
* @returns
|
|
1009
|
+
*/
|
|
740
1010
|
async function resolvePath (drive, pathname, noResolve) {
|
|
741
1011
|
if (noResolve) {
|
|
742
1012
|
const entry = await drive.entry(pathname)
|
|
@@ -754,6 +1024,12 @@ async function resolvePath (drive, pathname, noResolve) {
|
|
|
754
1024
|
return { entry: null, path: null }
|
|
755
1025
|
}
|
|
756
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
*
|
|
1029
|
+
* @param {Hyperdrive} drive
|
|
1030
|
+
* @param {string} [pathname]
|
|
1031
|
+
* @returns
|
|
1032
|
+
*/
|
|
757
1033
|
async function listEntries (drive, pathname = '/') {
|
|
758
1034
|
const entries = []
|
|
759
1035
|
for await (const path of drive.readdir(pathname)) {
|
|
@@ -768,9 +1044,15 @@ async function listEntries (drive, pathname = '/') {
|
|
|
768
1044
|
return entries
|
|
769
1045
|
}
|
|
770
1046
|
|
|
1047
|
+
/**
|
|
1048
|
+
*
|
|
1049
|
+
* @param {import('hypercore').Peer[]} peers
|
|
1050
|
+
* @returns
|
|
1051
|
+
*/
|
|
771
1052
|
function formatPeers (peers) {
|
|
772
1053
|
return peers.map((peer) => {
|
|
773
1054
|
const remotePublicKey = peer.remotePublicKey.toString('hex')
|
|
1055
|
+
// @ts-ignore
|
|
774
1056
|
const remoteHost = peer.stream?.rawStream?.remoteHost
|
|
775
1057
|
return {
|
|
776
1058
|
remotePublicKey,
|
|
@@ -779,12 +1061,22 @@ function formatPeers (peers) {
|
|
|
779
1061
|
})
|
|
780
1062
|
}
|
|
781
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
*
|
|
1066
|
+
* @param {string} path
|
|
1067
|
+
* @returns {string}
|
|
1068
|
+
*/
|
|
782
1069
|
function getMimeType (path) {
|
|
783
1070
|
let mimeType = mime.getType(path) || 'text/plain; charset=utf-8'
|
|
784
1071
|
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
|
|
785
1072
|
return mimeType
|
|
786
1073
|
}
|
|
787
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
*
|
|
1077
|
+
* @param {string} path
|
|
1078
|
+
* @returns {string}
|
|
1079
|
+
*/
|
|
788
1080
|
function ensureLeadingSlash (path) {
|
|
789
1081
|
return path.startsWith('/') ? path : '/' + path
|
|
790
1082
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore-fetch",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.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,15 +28,21 @@
|
|
|
24
28
|
"homepage": "https://github.com/RangerMauve/hypercore-fetch#readme",
|
|
25
29
|
"dependencies": {
|
|
26
30
|
"event-iterator": "^2.0.0",
|
|
27
|
-
"make-fetch": "^3.
|
|
31
|
+
"make-fetch": "^3.2.3",
|
|
28
32
|
"mime": "^3.0.0",
|
|
29
33
|
"range-parser": "^1.2.1",
|
|
30
34
|
"streamx": "^2.13.0"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
33
|
-
"@rangermauve/fetch-event-source": "^
|
|
34
|
-
"
|
|
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",
|
|
35
44
|
"standard": "^17.0.0",
|
|
36
|
-
"tape": "^5.2.2"
|
|
45
|
+
"tape": "^5.2.2",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
37
47
|
}
|
|
38
48
|
}
|
package/test.js
CHANGED
|
@@ -9,15 +9,28 @@ import { rm } from 'fs/promises'
|
|
|
9
9
|
|
|
10
10
|
import makeHyperFetch from './index.js'
|
|
11
11
|
|
|
12
|
+
/** @import {OnLoadHandler, OnDeleteHandler} from './index.js' */
|
|
13
|
+
|
|
12
14
|
const SAMPLE_CONTENT = 'Hello World'
|
|
13
15
|
const DNS_DOMAIN = 'blog.mauve.moe'
|
|
14
16
|
let count = 0
|
|
17
|
+
|
|
18
|
+
/** @type {OnLoadHandler | null} */
|
|
19
|
+
let onLoad = null
|
|
20
|
+
/** @type {OnDeleteHandler | null} */
|
|
21
|
+
let onDelete = null
|
|
22
|
+
|
|
15
23
|
function next () {
|
|
16
24
|
return count++
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
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}`, {
|
|
21
34
|
method: 'post'
|
|
22
35
|
})
|
|
23
36
|
await checkResponse(createResponse, t, 'Created new drive')
|
|
@@ -34,7 +47,9 @@ const sdk2 = await SDK.create({ storage: join(tmp, 'sdk2') })
|
|
|
34
47
|
|
|
35
48
|
const fetch = await makeHyperFetch({
|
|
36
49
|
sdk: sdk1,
|
|
37
|
-
writable: true
|
|
50
|
+
writable: true,
|
|
51
|
+
onLoad: (...args) => onLoad && onLoad(...args),
|
|
52
|
+
onDelete: (...args) => onDelete && onDelete(...args)
|
|
38
53
|
})
|
|
39
54
|
|
|
40
55
|
const fetch2 = await makeHyperFetch({
|
|
@@ -82,7 +97,7 @@ test('Quick check', async (t) => {
|
|
|
82
97
|
|
|
83
98
|
const content = await uploadedContentResponse.text()
|
|
84
99
|
const contentType = uploadedContentResponse.headers.get('Content-Type')
|
|
85
|
-
const contentLink = uploadedContentResponse.headers.get('Link')
|
|
100
|
+
const contentLink = uploadedContentResponse.headers.get('Link') ?? ''
|
|
86
101
|
|
|
87
102
|
t.match(contentLink, /^<hyper:\/\/[0-9a-z]{52}\/example%20.txt>; rel="canonical"$/, 'Link header includes both public key and path.')
|
|
88
103
|
t.equal(contentType, 'text/plain; charset=utf-8', 'Content got expected mime type')
|
|
@@ -127,7 +142,7 @@ test('HEAD request', async (t) => {
|
|
|
127
142
|
const headersContentLength = headResponse.headers.get('Content-Length')
|
|
128
143
|
const headersAcceptRanges = headResponse.headers.get('Accept-Ranges')
|
|
129
144
|
const headersLastModified = headResponse.headers.get('Last-Modified')
|
|
130
|
-
const headersLink = headResponse.headers.get('Link')
|
|
145
|
+
const headersLink = headResponse.headers.get('Link') ?? ''
|
|
131
146
|
|
|
132
147
|
t.equal(headResponse.status, 204, 'Response had expected status')
|
|
133
148
|
// Version at which the file was added
|
|
@@ -144,7 +159,7 @@ test('PUT file', async (t) => {
|
|
|
144
159
|
|
|
145
160
|
const uploadLocation = new URL('./example.txt', created)
|
|
146
161
|
|
|
147
|
-
const fakeDate = new Date(
|
|
162
|
+
const fakeDate = new Date().toUTCString()
|
|
148
163
|
const uploadResponse = await fetch(uploadLocation, {
|
|
149
164
|
method: 'put',
|
|
150
165
|
body: SAMPLE_CONTENT,
|
|
@@ -464,12 +479,12 @@ test('EventSource extension messages', async (t) => {
|
|
|
464
479
|
t.deepEqual(extensionList, ['example'], 'Got expected list of extensions')
|
|
465
480
|
|
|
466
481
|
const peerResponse1 = await fetch(extensionURL)
|
|
467
|
-
const peerList1 = await peerResponse1.json()
|
|
482
|
+
const peerList1 = /** @type {object[]} */ (await peerResponse1.json())
|
|
468
483
|
|
|
469
484
|
t.equal(peerList1.length, 1, 'Got one peer for extension message on peer1')
|
|
470
485
|
|
|
471
486
|
const peerResponse2 = await fetch2(extensionURL)
|
|
472
|
-
const peerList2 = await peerResponse2.json()
|
|
487
|
+
const peerList2 = /** @type {object[]} */ (await peerResponse2.json())
|
|
473
488
|
|
|
474
489
|
t.equal(peerList2.length, 1, 'Got one peer for extension message on peer2')
|
|
475
490
|
|
|
@@ -502,11 +517,13 @@ test('EventSource extension messages', async (t) => {
|
|
|
502
517
|
test('Resolve DNS', async (t) => {
|
|
503
518
|
const loadResponse = await fetch(`hyper://${DNS_DOMAIN}/?noResolve`)
|
|
504
519
|
|
|
505
|
-
const entries = await loadResponse.json()
|
|
520
|
+
const entries = /** @type {object[]} */ (await loadResponse.json())
|
|
506
521
|
|
|
507
522
|
t.ok(entries.length, 'Loaded contents with some files present')
|
|
508
523
|
|
|
509
|
-
const
|
|
524
|
+
const linkHeader = loadResponse.headers.get('Link') || ''
|
|
525
|
+
// @ts-ignore
|
|
526
|
+
const rawLink = linkHeader.match(/<(.+)>/)[1]
|
|
510
527
|
const loadRawURLResponse = await fetch(rawLink + '?noResolve')
|
|
511
528
|
|
|
512
529
|
const rawLinkEntries = await loadRawURLResponse.json()
|
|
@@ -652,10 +669,59 @@ test('Check hyperdrive writability', async (t) => {
|
|
|
652
669
|
t.equal(writableHeadersAllow, 'HEAD,GET,PUT,DELETE', 'Expected writable Allows header')
|
|
653
670
|
})
|
|
654
671
|
|
|
672
|
+
test('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
|
+
*/
|
|
655
721
|
async function checkResponse (response, t, successMessage = 'Response OK') {
|
|
656
722
|
if (!response.ok) {
|
|
657
723
|
const message = await response.text()
|
|
658
|
-
t.fail(
|
|
724
|
+
t.fail(`HTTP Error ${response.status}:\n${message}`)
|
|
659
725
|
return false
|
|
660
726
|
} else {
|
|
661
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
|
+
}
|