hypercore-fetch 9.0.8 → 9.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 +12 -0
- package/index.js +260 -183
- package/package.json +1 -1
- package/test.js +33 -6
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ Each response will contain a header for the canonical URL represented as a `Link
|
|
|
48
48
|
|
|
49
49
|
There is also an `ETag` header which will be a JSON string containging the drive's current `version`, or the file's sequence number.
|
|
50
50
|
This will change only when the drive has gotten an update of some sort and is monotonically incrementing.
|
|
51
|
+
The `ETag` representing a file's sequence number represents the version the Hyperdrive was at when the file was added.
|
|
52
|
+
Thus you can get the previous version of a file by using `hyper://NAME/$/version/${ETAG}/example.txt`.
|
|
51
53
|
|
|
52
54
|
If the resource is a file, it may contain the `Last-Modified` header if the file has had a `metadata.mtime` flag set upon update.
|
|
53
55
|
|
|
@@ -188,3 +190,13 @@ The `body` of the request will be used as the payload.
|
|
|
188
190
|
Please note that only utf8 encoded text is currently supported due to limitations of the event-stream encoding.
|
|
189
191
|
|
|
190
192
|
Note that this requires the `extensionMessages: true` flag.
|
|
193
|
+
|
|
194
|
+
### `fetch('hyper://NAME/$/version/VERSION_NUMBER/example.txt')`
|
|
195
|
+
|
|
196
|
+
You can get older views of data in an archive by using the special `/$/version` folder with a version number to view older states.
|
|
197
|
+
|
|
198
|
+
`VERSION_NUMBER` should be a number representing the version to check out based on the `ETag` of the root of the archive.
|
|
199
|
+
|
|
200
|
+
From there, you can use `GET` and `HEAD` requests with allt he same headers and querystring paramters as non-versioned paths to data.
|
|
201
|
+
|
|
202
|
+
Note that you cannot `PUT` or `DELETE` data in a versioned folder.
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const SPECIAL_DOMAIN = 'localhost'
|
|
|
13
13
|
const SPECIAL_FOLDER = '$'
|
|
14
14
|
const EXTENSIONS_FOLDER_NAME = 'extensions'
|
|
15
15
|
const EXTENSION_EVENT = 'extension-message'
|
|
16
|
+
const VERSION_FOLDER_NAME = 'version'
|
|
16
17
|
const PEER_OPEN = 'peer-open'
|
|
17
18
|
const PEER_REMOVE = 'peer-remove'
|
|
18
19
|
|
|
@@ -39,6 +40,11 @@ async function DEFAULT_RENDER_INDEX (url, files, fetch) {
|
|
|
39
40
|
`
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// Support gemini files
|
|
44
|
+
mime.define({
|
|
45
|
+
'text/gemini': ['gmi', 'gemini']
|
|
46
|
+
}, true)
|
|
47
|
+
|
|
42
48
|
export default async function makeHyperFetch ({
|
|
43
49
|
sdk,
|
|
44
50
|
writable = false,
|
|
@@ -54,6 +60,26 @@ export default async function makeHyperFetch ({
|
|
|
54
60
|
const extensions = new Map()
|
|
55
61
|
const cores = new Map()
|
|
56
62
|
|
|
63
|
+
if (extensionMessages) {
|
|
64
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`, listExtensions)
|
|
65
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, listenExtension)
|
|
66
|
+
router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, broadcastExtension)
|
|
67
|
+
router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*/*`, extensionToPeer)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (writable) {
|
|
71
|
+
router.get(`hyper://${SPECIAL_DOMAIN}/`, getKey)
|
|
72
|
+
router.post(`hyper://${SPECIAL_DOMAIN}/`, createKey)
|
|
73
|
+
|
|
74
|
+
router.put('hyper://*/**', putFiles)
|
|
75
|
+
router.delete('hyper://*/**', deleteFiles)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
router.head(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, headFilesVersioned)
|
|
79
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${VERSION_FOLDER_NAME}/**`, getFilesVersioned)
|
|
80
|
+
router.get('hyper://*/**', getFiles)
|
|
81
|
+
router.head('hyper://*/**', headFiles)
|
|
82
|
+
|
|
57
83
|
async function getCore (hostname) {
|
|
58
84
|
if (cores.has(hostname)) {
|
|
59
85
|
return cores.get(hostname)
|
|
@@ -155,225 +181,246 @@ export default async function makeHyperFetch ({
|
|
|
155
181
|
return [...core.extensions.keys()]
|
|
156
182
|
}
|
|
157
183
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const accept = request.headers.get('Accept') || ''
|
|
184
|
+
async function listExtensions (request) {
|
|
185
|
+
const { hostname } = new URL(request.url)
|
|
186
|
+
const accept = request.headers.get('Accept') || ''
|
|
162
187
|
|
|
163
|
-
|
|
188
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
164
189
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
if (accept.includes('text/event-stream')) {
|
|
191
|
+
const events = new EventIterator(({ push }) => {
|
|
192
|
+
function onMessage (name, content, peer) {
|
|
193
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
194
|
+
// TODO: Fancy verification on the `name`?
|
|
195
|
+
// Send each line of content separately on a `data` line
|
|
196
|
+
const data = content.split('\n').map((line) => `data:${line}\n`).join('')
|
|
172
197
|
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
function onPeerOpen (peer) {
|
|
176
|
-
const id = peer.remotePublicKey.toString('hex')
|
|
177
|
-
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
178
|
-
}
|
|
179
|
-
function onPeerRemove (peer) {
|
|
180
|
-
// Whatever, probably an uninitialized peer
|
|
181
|
-
if (!peer.remotePublicKey) return
|
|
182
|
-
const id = peer.remotePublicKey.toString('hex')
|
|
183
|
-
push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
|
|
184
|
-
}
|
|
185
|
-
core.on(EXTENSION_EVENT, onMessage)
|
|
186
|
-
core.on(PEER_OPEN, onPeerOpen)
|
|
187
|
-
core.on(PEER_REMOVE, onPeerRemove)
|
|
188
|
-
return () => {
|
|
189
|
-
core.removeListener(EXTENSION_EVENT, onMessage)
|
|
190
|
-
core.removeListener(PEER_OPEN, onPeerOpen)
|
|
191
|
-
core.removeListener(PEER_REMOVE, onPeerRemove)
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
statusCode: 200,
|
|
197
|
-
headers: {
|
|
198
|
-
[HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
|
|
199
|
-
},
|
|
200
|
-
body: events
|
|
198
|
+
push(`id:${id}\nevent:${name}\n${data}\n`)
|
|
201
199
|
}
|
|
202
|
-
|
|
200
|
+
function onPeerOpen (peer) {
|
|
201
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
202
|
+
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
203
|
+
}
|
|
204
|
+
function onPeerRemove (peer) {
|
|
205
|
+
// Whatever, probably an uninitialized peer
|
|
206
|
+
if (!peer.remotePublicKey) return
|
|
207
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
208
|
+
push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
|
|
209
|
+
}
|
|
210
|
+
core.on(EXTENSION_EVENT, onMessage)
|
|
211
|
+
core.on(PEER_OPEN, onPeerOpen)
|
|
212
|
+
core.on(PEER_REMOVE, onPeerRemove)
|
|
213
|
+
return () => {
|
|
214
|
+
core.removeListener(EXTENSION_EVENT, onMessage)
|
|
215
|
+
core.removeListener(PEER_OPEN, onPeerOpen)
|
|
216
|
+
core.removeListener(PEER_REMOVE, onPeerRemove)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
203
219
|
|
|
204
|
-
const extensions = listExtensionNames(core)
|
|
205
220
|
return {
|
|
206
|
-
|
|
207
|
-
headers: {
|
|
208
|
-
|
|
221
|
+
statusCode: 200,
|
|
222
|
+
headers: {
|
|
223
|
+
[HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
|
|
224
|
+
},
|
|
225
|
+
body: events
|
|
209
226
|
}
|
|
210
|
-
}
|
|
211
|
-
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, async function listenExtension (request) {
|
|
212
|
-
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
213
|
-
const pathname = decodeURI(rawPathname)
|
|
214
|
-
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
227
|
+
}
|
|
215
228
|
|
|
216
|
-
|
|
229
|
+
const extensions = listExtensionNames(core)
|
|
230
|
+
return {
|
|
231
|
+
status: 200,
|
|
232
|
+
headers: { [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON },
|
|
233
|
+
body: JSON.stringify(extensions, null, '\t')
|
|
234
|
+
}
|
|
235
|
+
}
|
|
217
236
|
|
|
218
|
-
|
|
237
|
+
async function listenExtension (request) {
|
|
238
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
239
|
+
const pathname = decodeURI(rawPathname)
|
|
240
|
+
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
219
241
|
|
|
220
|
-
|
|
221
|
-
const finalPeers = formatPeers(peers)
|
|
222
|
-
const body = JSON.stringify(finalPeers, null, '\t')
|
|
242
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
223
243
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
await getExtension(core, name)
|
|
245
|
+
|
|
246
|
+
const peers = await getExtensionPeers(core, name)
|
|
247
|
+
const finalPeers = formatPeers(peers)
|
|
248
|
+
const body = JSON.stringify(finalPeers, null, '\t')
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
status: 200,
|
|
252
|
+
body,
|
|
253
|
+
headers: {
|
|
254
|
+
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
|
|
230
255
|
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
234
|
-
const pathname = decodeURI(rawPathname)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
235
258
|
|
|
236
|
-
|
|
259
|
+
async function broadcastExtension (request) {
|
|
260
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
261
|
+
const pathname = decodeURI(rawPathname)
|
|
237
262
|
|
|
238
|
-
|
|
263
|
+
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
239
264
|
|
|
240
|
-
|
|
241
|
-
const data = await request.text()
|
|
242
|
-
extension.broadcast(data)
|
|
265
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
243
266
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
248
|
-
const pathname = decodeURI(rawPathname)
|
|
267
|
+
const extension = await getExtension(core, name)
|
|
268
|
+
const data = await request.text()
|
|
269
|
+
extension.broadcast(data)
|
|
249
270
|
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
return { status: 200 }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function extensionToPeer (request) {
|
|
275
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
276
|
+
const pathname = decodeURI(rawPathname)
|
|
252
277
|
|
|
253
|
-
|
|
278
|
+
const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
279
|
+
const [name, extensionPeer] = subFolder.split('/')
|
|
254
280
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
282
|
+
|
|
283
|
+
const extension = await getExtension(core, name)
|
|
284
|
+
const peers = await getExtensionPeers(core, name)
|
|
285
|
+
const peer = peers.find(({ remotePublicKey }) => remotePublicKey.toString('hex') === extensionPeer)
|
|
286
|
+
if (!peer) {
|
|
287
|
+
return {
|
|
288
|
+
status: 404,
|
|
289
|
+
headers: {
|
|
290
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
291
|
+
},
|
|
292
|
+
body: 'Peer Not Found'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const data = await request.arrayBuffer()
|
|
296
|
+
extension.send(data, peer)
|
|
297
|
+
return { status: 200 }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function getKey (request) {
|
|
301
|
+
const key = new URL(request.url).searchParams.get('key')
|
|
302
|
+
if (!key) {
|
|
303
|
+
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const drive = await getDriveFromKey(key, true)
|
|
308
|
+
|
|
309
|
+
return { body: drive.url }
|
|
310
|
+
} catch (e) {
|
|
311
|
+
if (e.message === ERROR_KEY_NOT_CREATED) {
|
|
259
312
|
return {
|
|
260
|
-
status:
|
|
313
|
+
status: 400,
|
|
314
|
+
body: e.message,
|
|
261
315
|
headers: {
|
|
262
316
|
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
263
|
-
}
|
|
264
|
-
body: 'Peer Not Found'
|
|
317
|
+
}
|
|
265
318
|
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
extension.send(data, peer)
|
|
269
|
-
return { status: 200 }
|
|
270
|
-
})
|
|
319
|
+
} else throw e
|
|
320
|
+
}
|
|
271
321
|
}
|
|
272
322
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
278
|
-
}
|
|
323
|
+
async function createKey (request) {
|
|
324
|
+
// TODO: Allow importing secret keys here
|
|
325
|
+
// Maybe specify a seed to use for generating the blobs?
|
|
326
|
+
// Else we'd need to specify the blobs keys and metadata keys
|
|
279
327
|
|
|
280
|
-
|
|
281
|
-
|
|
328
|
+
const key = new URL(request.url).searchParams.get('key')
|
|
329
|
+
if (!key) {
|
|
330
|
+
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
331
|
+
}
|
|
282
332
|
|
|
283
|
-
|
|
284
|
-
} catch (e) {
|
|
285
|
-
if (e.message === ERROR_KEY_NOT_CREATED) {
|
|
286
|
-
return {
|
|
287
|
-
status: 400,
|
|
288
|
-
body: e.message,
|
|
289
|
-
headers: {
|
|
290
|
-
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
} else throw e
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
router.post(`hyper://${SPECIAL_DOMAIN}/`, async function createKey (request) {
|
|
297
|
-
// TODO: Allow importing secret keys here
|
|
298
|
-
// Maybe specify a seed to use for generating the blobs?
|
|
299
|
-
// Else we'd need to specify the blobs keys and metadata keys
|
|
300
|
-
|
|
301
|
-
const key = new URL(request.url).searchParams.get('key')
|
|
302
|
-
if (!key) {
|
|
303
|
-
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
304
|
-
}
|
|
333
|
+
const drive = await getDriveFromKey(key, false)
|
|
305
334
|
|
|
306
|
-
|
|
335
|
+
return { body: drive.url }
|
|
336
|
+
}
|
|
307
337
|
|
|
308
|
-
|
|
309
|
-
})
|
|
338
|
+
async function putFiles (request) {
|
|
339
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
340
|
+
const pathname = decodeURI(rawPathname)
|
|
341
|
+
const contentType = request.headers.get('Content-Type') || ''
|
|
342
|
+
const isFormData = contentType.includes('multipart/form-data')
|
|
310
343
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (isFormData) {
|
|
320
|
-
// It's a form! Get the files out and process them
|
|
321
|
-
const formData = await request.formData()
|
|
322
|
-
for (const [name, data] of formData) {
|
|
323
|
-
if (name !== 'file') continue
|
|
324
|
-
const filePath = posix.join(pathname, data.name)
|
|
325
|
-
await pipelinePromise(
|
|
326
|
-
Readable.from(data.stream()),
|
|
327
|
-
drive.createWriteStream(filePath, {
|
|
328
|
-
metadata: {
|
|
329
|
-
mtime: Date.now()
|
|
330
|
-
}
|
|
331
|
-
})
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
344
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
345
|
+
|
|
346
|
+
if (isFormData) {
|
|
347
|
+
// It's a form! Get the files out and process them
|
|
348
|
+
const formData = await request.formData()
|
|
349
|
+
for (const [name, data] of formData) {
|
|
350
|
+
if (name !== 'file') continue
|
|
351
|
+
const filePath = posix.join(pathname, data.name)
|
|
335
352
|
await pipelinePromise(
|
|
336
|
-
Readable.from(
|
|
337
|
-
drive.createWriteStream(
|
|
353
|
+
Readable.from(data.stream()),
|
|
354
|
+
drive.createWriteStream(filePath, {
|
|
338
355
|
metadata: {
|
|
339
356
|
mtime: Date.now()
|
|
340
357
|
}
|
|
341
358
|
})
|
|
342
359
|
)
|
|
343
360
|
}
|
|
361
|
+
} else {
|
|
362
|
+
await pipelinePromise(
|
|
363
|
+
Readable.from(request.body),
|
|
364
|
+
drive.createWriteStream(pathname, {
|
|
365
|
+
metadata: {
|
|
366
|
+
mtime: Date.now()
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
)
|
|
370
|
+
}
|
|
344
371
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
router.delete('hyper://*/**', async function putFiles (request) {
|
|
348
|
-
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
349
|
-
const pathname = decodeURI(rawPathname)
|
|
350
|
-
|
|
351
|
-
const drive = await getDrive(`hyper://${hostname}`)
|
|
372
|
+
return { status: 201, headers: { Location: request.url } }
|
|
373
|
+
}
|
|
352
374
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
await drive.del(entry.key)
|
|
357
|
-
didDelete = true
|
|
358
|
-
}
|
|
359
|
-
if (!didDelete) {
|
|
360
|
-
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
361
|
-
}
|
|
362
|
-
return { status: 200 }
|
|
363
|
-
}
|
|
375
|
+
async function deleteFiles (request) {
|
|
376
|
+
const { hostname, pathname: rawPathname } = new URL(request.url)
|
|
377
|
+
const pathname = decodeURI(rawPathname)
|
|
364
378
|
|
|
365
|
-
|
|
379
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
366
380
|
|
|
367
|
-
|
|
381
|
+
if (pathname.endsWith('/')) {
|
|
382
|
+
let didDelete = false
|
|
383
|
+
for await (const entry of drive.list(pathname)) {
|
|
384
|
+
await drive.del(entry.key)
|
|
385
|
+
didDelete = true
|
|
386
|
+
}
|
|
387
|
+
if (!didDelete) {
|
|
368
388
|
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
369
389
|
}
|
|
370
|
-
await drive.del(pathname)
|
|
371
|
-
|
|
372
390
|
return { status: 200 }
|
|
373
|
-
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const entry = await drive.entry(pathname)
|
|
394
|
+
|
|
395
|
+
if (!entry) {
|
|
396
|
+
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
397
|
+
}
|
|
398
|
+
await drive.del(pathname)
|
|
399
|
+
|
|
400
|
+
return { status: 200 }
|
|
374
401
|
}
|
|
375
402
|
|
|
376
|
-
|
|
403
|
+
async function headFilesVersioned (request) {
|
|
404
|
+
const url = new URL(request.url)
|
|
405
|
+
const { hostname, pathname: rawPathname, searchParams } = url
|
|
406
|
+
const pathname = decodeURI(rawPathname)
|
|
407
|
+
|
|
408
|
+
const accept = request.headers.get('Accept') || ''
|
|
409
|
+
const isRanged = request.headers.get('Range') || ''
|
|
410
|
+
const noResolve = searchParams.has('noResolve')
|
|
411
|
+
|
|
412
|
+
const parts = pathname.split('/')
|
|
413
|
+
const version = parts[3]
|
|
414
|
+
const realPath = parts.slice(4).join('/')
|
|
415
|
+
|
|
416
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
417
|
+
|
|
418
|
+
const snapshot = await drive.checkout(version)
|
|
419
|
+
|
|
420
|
+
return serveHead(snapshot, realPath, { accept, isRanged, noResolve })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function headFiles (request) {
|
|
377
424
|
const url = new URL(request.url)
|
|
378
425
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
379
426
|
const pathname = decodeURI(rawPathname)
|
|
@@ -381,9 +428,14 @@ export default async function makeHyperFetch ({
|
|
|
381
428
|
const accept = request.headers.get('Accept') || ''
|
|
382
429
|
const isRanged = request.headers.get('Range') || ''
|
|
383
430
|
const noResolve = searchParams.has('noResolve')
|
|
384
|
-
const isDirectory = pathname.endsWith('/')
|
|
385
431
|
|
|
386
432
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
433
|
+
|
|
434
|
+
return serveHead(drive, pathname, { accept, isRanged, noResolve })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function serveHead (drive, pathname, { accept, isRanged, noResolve }) {
|
|
438
|
+
const isDirectory = pathname.endsWith('/')
|
|
387
439
|
const fullURL = new URL(pathname, drive.url).href
|
|
388
440
|
|
|
389
441
|
const resHeaders = {
|
|
@@ -478,19 +530,45 @@ export default async function makeHyperFetch ({
|
|
|
478
530
|
status: 200,
|
|
479
531
|
headers: resHeaders
|
|
480
532
|
}
|
|
481
|
-
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function getFilesVersioned (request) {
|
|
536
|
+
const url = new URL(request.url)
|
|
537
|
+
const { hostname, pathname: rawPathname, searchParams } = url
|
|
538
|
+
const pathname = decodeURI(rawPathname)
|
|
539
|
+
|
|
540
|
+
const accept = request.headers.get('Accept') || ''
|
|
541
|
+
const isRanged = request.headers.get('Range') || ''
|
|
542
|
+
const noResolve = searchParams.has('noResolve')
|
|
543
|
+
|
|
544
|
+
const parts = pathname.split('/')
|
|
545
|
+
const version = parts[3]
|
|
546
|
+
const realPath = parts.slice(4).join('/')
|
|
547
|
+
|
|
548
|
+
const drive = await getDrive(`hyper://${hostname}`)
|
|
549
|
+
|
|
550
|
+
const snapshot = await drive.checkout(version)
|
|
551
|
+
|
|
552
|
+
return serveGet(snapshot, realPath, { accept, isRanged, noResolve })
|
|
553
|
+
}
|
|
482
554
|
|
|
483
555
|
// TODO: Redirect on directories without trailing slash
|
|
484
|
-
|
|
556
|
+
async function getFiles (request) {
|
|
485
557
|
const url = new URL(request.url)
|
|
486
558
|
const { hostname, pathname: rawPathname, searchParams } = url
|
|
487
559
|
const pathname = decodeURI(rawPathname)
|
|
488
560
|
|
|
489
561
|
const accept = request.headers.get('Accept') || ''
|
|
562
|
+
const isRanged = request.headers.get('Range') || ''
|
|
490
563
|
const noResolve = searchParams.has('noResolve')
|
|
491
|
-
const isDirectory = pathname.endsWith('/')
|
|
492
564
|
|
|
493
565
|
const drive = await getDrive(`hyper://${hostname}`)
|
|
566
|
+
|
|
567
|
+
return serveGet(drive, pathname, { accept, isRanged, noResolve })
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function serveGet (drive, pathname, { accept, isRanged, noResolve }) {
|
|
571
|
+
const isDirectory = pathname.endsWith('/')
|
|
494
572
|
const fullURL = new URL(pathname, drive.url).href
|
|
495
573
|
|
|
496
574
|
if (isDirectory) {
|
|
@@ -514,12 +592,12 @@ export default async function makeHyperFetch ({
|
|
|
514
592
|
|
|
515
593
|
if (!noResolve) {
|
|
516
594
|
if (entries.includes('index.html')) {
|
|
517
|
-
return serveFile(
|
|
595
|
+
return serveFile(drive, posix.join(pathname, 'index.html'), isRanged)
|
|
518
596
|
}
|
|
519
597
|
}
|
|
520
598
|
|
|
521
599
|
if (accept.includes('text/html')) {
|
|
522
|
-
const body = await renderIndex(
|
|
600
|
+
const body = await renderIndex(new URL(fullURL), entries, fetch)
|
|
523
601
|
return {
|
|
524
602
|
status: 200,
|
|
525
603
|
body,
|
|
@@ -546,14 +624,13 @@ export default async function makeHyperFetch ({
|
|
|
546
624
|
return { status: 404, body: 'Not Found' }
|
|
547
625
|
}
|
|
548
626
|
|
|
549
|
-
return serveFile(
|
|
550
|
-
}
|
|
627
|
+
return serveFile(drive, path, isRanged)
|
|
628
|
+
}
|
|
551
629
|
|
|
552
630
|
return fetch
|
|
553
631
|
}
|
|
554
632
|
|
|
555
|
-
async function serveFile (
|
|
556
|
-
const isRanged = headers.get('Range') || ''
|
|
633
|
+
async function serveFile (drive, pathname, isRanged) {
|
|
557
634
|
const contentType = getMimeType(pathname)
|
|
558
635
|
|
|
559
636
|
const fullURL = new URL(pathname, drive.url).href
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -447,30 +447,57 @@ test('EventSource extension messages', async (t) => {
|
|
|
447
447
|
})
|
|
448
448
|
|
|
449
449
|
test('Resolve DNS', async (t) => {
|
|
450
|
-
const loadResponse = await fetch('hyper://example2.mauve.moe
|
|
450
|
+
const loadResponse = await fetch('hyper://example2.mauve.moe/?noResolve')
|
|
451
451
|
|
|
452
452
|
const entries = await loadResponse.json()
|
|
453
453
|
|
|
454
|
-
t.
|
|
454
|
+
t.ok(entries.length, 'Loaded contents with some files present')
|
|
455
455
|
})
|
|
456
456
|
|
|
457
457
|
test('Error on invalid hostname', async (t) => {
|
|
458
458
|
const loadResponse = await fetch('hyper://example/')
|
|
459
459
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if(loadResponse.ok) {
|
|
460
|
+
if (loadResponse.ok) {
|
|
463
461
|
throw new Error('Loading without DNS or a public key should have failed')
|
|
464
462
|
} else {
|
|
465
|
-
t.pass(
|
|
463
|
+
t.pass('Invalid names led to an error')
|
|
466
464
|
}
|
|
467
465
|
})
|
|
468
466
|
|
|
467
|
+
test('GET older version of file from VERSION folder', async (t) => {
|
|
468
|
+
const created = await nextURL(t)
|
|
469
|
+
|
|
470
|
+
const fileName = 'example.txt'
|
|
471
|
+
|
|
472
|
+
const data1 = 'Hello World'
|
|
473
|
+
const data2 = 'Goodbye World'
|
|
474
|
+
|
|
475
|
+
const fileURL = new URL(`/${fileName}`, created)
|
|
476
|
+
const versionFileURL = new URL(`/$/version/2/${fileName}`, created)
|
|
477
|
+
|
|
478
|
+
await checkResponse(
|
|
479
|
+
await fetch(fileURL, { method: 'PUT', body: data1 }), t
|
|
480
|
+
)
|
|
481
|
+
await checkResponse(
|
|
482
|
+
await fetch(fileURL, { method: 'PUT', body: data2 }), t
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
const versionedResponse = await fetch(versionFileURL)
|
|
486
|
+
|
|
487
|
+
await checkResponse(versionedResponse, t, 'Able to GET versioned file')
|
|
488
|
+
|
|
489
|
+
const versionedData = await versionedResponse.text()
|
|
490
|
+
|
|
491
|
+
t.equal(versionedData, data1, 'Old data got loaded')
|
|
492
|
+
})
|
|
493
|
+
|
|
469
494
|
async function checkResponse (response, t, successMessage = 'Response OK') {
|
|
470
495
|
if (!response.ok) {
|
|
471
496
|
const message = await response.text()
|
|
472
497
|
t.fail(new Error(`HTTP Error ${response.status}:\n${message}`))
|
|
498
|
+
return false
|
|
473
499
|
} else {
|
|
474
500
|
t.pass(successMessage)
|
|
501
|
+
return true
|
|
475
502
|
}
|
|
476
503
|
}
|