hypercore-fetch 8.6.1 → 9.0.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/.github/workflows/test.yml +1 -1
- package/LICENSE +0 -0
- package/README.md +67 -116
- package/index.js +501 -663
- package/package.json +8 -16
- package/test.js +287 -182
- package/bin.js +0 -27
package/index.js
CHANGED
|
@@ -1,787 +1,625 @@
|
|
|
1
|
-
|
|
2
|
-
const Headers = require('fetch-headers')
|
|
3
|
-
const mime = require('mime/lite')
|
|
4
|
-
const SDK = require('hyper-sdk')
|
|
5
|
-
const parseRange = require('range-parser')
|
|
6
|
-
const makeDir = require('make-dir')
|
|
7
|
-
const { Readable } = require('streamx')
|
|
8
|
-
const makeFetch = require('make-fetch')
|
|
9
|
-
const { EventIterator } = require('event-iterator')
|
|
10
|
-
const Busboy = require('busboy')
|
|
11
|
-
const posixPath = require('path').posix
|
|
1
|
+
import { posix } from 'path'
|
|
12
2
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
import { Readable, pipelinePromise } from 'streamx'
|
|
4
|
+
import Hyperdrive from 'hyperdrive'
|
|
5
|
+
import { makeRoutedFetch } from 'make-fetch'
|
|
6
|
+
import mime from 'mime/lite.js'
|
|
7
|
+
import parseRange from 'range-parser'
|
|
8
|
+
import { EventIterator } from 'event-iterator'
|
|
18
9
|
|
|
19
|
-
const
|
|
20
|
-
const WRITABLE_ALLOW = ['PUT', 'POST', 'DELETE']
|
|
21
|
-
const ALL_ALLOW = READABLE_ALLOW.concat(WRITABLE_ALLOW)
|
|
10
|
+
const DEFAULT_TIMEOUT = 5000
|
|
22
11
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const EXTENSIONS_FOLDER_NAME = 'extensions/'
|
|
27
|
-
const EXTENSIONS_FOLDER = SPECIAL_FOLDER + EXTENSIONS_FOLDER_NAME
|
|
12
|
+
const SPECIAL_DOMAIN = 'localhost'
|
|
13
|
+
const SPECIAL_FOLDER = '$'
|
|
14
|
+
const EXTENSIONS_FOLDER_NAME = 'extensions'
|
|
28
15
|
const EXTENSION_EVENT = 'extension-message'
|
|
29
16
|
const PEER_OPEN = 'peer-open'
|
|
30
17
|
const PEER_REMOVE = 'peer-remove'
|
|
31
18
|
|
|
32
|
-
|
|
33
|
-
const
|
|
19
|
+
const MIME_TEXT_PLAIN = 'text/plain; charset=utf-8'
|
|
20
|
+
const MIME_APPLICATION_JSON = 'application/json'
|
|
21
|
+
const MIME_TEXT_HTML = 'text/html; charset=utf-8'
|
|
22
|
+
const MIME_EVENT_STREAM = 'text/event-stream; charset=utf-8'
|
|
23
|
+
|
|
24
|
+
const HEADER_CONTENT_TYPE = 'Content-Type'
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
async function DEFAULT_RENDER_INDEX (url, files, fetch) {
|
|
27
|
+
return `
|
|
28
|
+
<!DOCTYPE html>
|
|
29
|
+
<title>Index of ${url.pathname}</title>
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
31
|
+
<h1>Index of ${url.pathname}</h1>
|
|
32
|
+
<ul>
|
|
33
|
+
<li><a href="../">../</a></li>
|
|
34
|
+
${files.map((file) => `<li><a href="${file}">./${file}</a></li>`).join('\n')}
|
|
35
|
+
</ul>
|
|
36
|
+
`
|
|
37
|
+
}
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
export default async function makeHyperFetch ({
|
|
40
|
+
sdk,
|
|
41
|
+
writable = false,
|
|
42
|
+
extensionMessages = writable,
|
|
43
|
+
timeout = DEFAULT_TIMEOUT,
|
|
44
|
+
renderIndex = DEFAULT_RENDER_INDEX
|
|
45
|
+
}) {
|
|
46
|
+
const { fetch, router } = makeRoutedFetch()
|
|
47
|
+
|
|
48
|
+
// Map loaded drive hostnames to their keys
|
|
49
|
+
// TODO: Track LRU + cache clearing
|
|
50
|
+
const drives = new Map()
|
|
51
|
+
const extensions = new Map()
|
|
52
|
+
const cores = new Map()
|
|
53
|
+
|
|
54
|
+
async function getCore (hostname) {
|
|
55
|
+
if (cores.has(hostname)) {
|
|
56
|
+
return cores.get(hostname)
|
|
57
|
+
}
|
|
58
|
+
const core = await sdk.get(hostname)
|
|
59
|
+
await core.ready()
|
|
60
|
+
cores.set(core.id, core)
|
|
61
|
+
cores.set(core.url, core)
|
|
62
|
+
return core
|
|
63
|
+
}
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
async function getDBCoreForName (name) {
|
|
66
|
+
const corestore = sdk.namespace(name)
|
|
67
|
+
const dbCore = corestore.get({ name: 'db' })
|
|
68
|
+
await dbCore.ready()
|
|
69
|
+
|
|
70
|
+
if (!dbCore.discovery) {
|
|
71
|
+
const discovery = sdk.join(dbCore.discoveryKey)
|
|
72
|
+
dbCore.discovery = discovery
|
|
73
|
+
dbCore.once('close', () => {
|
|
74
|
+
discovery.destroy()
|
|
75
|
+
})
|
|
76
|
+
await discovery.flushed()
|
|
77
|
+
}
|
|
49
78
|
|
|
50
|
-
|
|
79
|
+
return dbCore
|
|
80
|
+
}
|
|
51
81
|
|
|
52
|
-
|
|
82
|
+
async function getDrive (hostname) {
|
|
83
|
+
if (drives.has(hostname)) {
|
|
84
|
+
return drives.get(hostname)
|
|
85
|
+
}
|
|
53
86
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
const core = await getCore(hostname)
|
|
88
|
+
|
|
89
|
+
const corestore = sdk.namespace(core.id)
|
|
90
|
+
const drive = new Hyperdrive(corestore, core.key)
|
|
91
|
+
|
|
92
|
+
await drive.ready()
|
|
93
|
+
|
|
94
|
+
drives.set(drive.core.id, drive)
|
|
95
|
+
drives.set(hostname, drive)
|
|
96
|
+
|
|
97
|
+
return drive
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getDriveFromKey (key, errorOnNew = false) {
|
|
101
|
+
if (drives.has(key)) {
|
|
102
|
+
return drives.get(key)
|
|
103
|
+
}
|
|
104
|
+
const core = await getDBCoreForName(key)
|
|
105
|
+
if (!core.length && errorOnNew) {
|
|
106
|
+
return { status: 400, body: 'Must create key with POST before reading' }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const corestore = sdk.namespace(key)
|
|
110
|
+
const drive = new Hyperdrive(corestore)
|
|
111
|
+
|
|
112
|
+
await drive.ready()
|
|
113
|
+
|
|
114
|
+
drives.set(key, drive)
|
|
115
|
+
drives.set(drive.core.id, drive)
|
|
116
|
+
|
|
117
|
+
return drive
|
|
118
|
+
}
|
|
57
119
|
|
|
58
|
-
|
|
120
|
+
async function getExtension (core, name) {
|
|
121
|
+
const key = core.url + name
|
|
122
|
+
if (extensions.has(key)) {
|
|
123
|
+
return extensions.get(key)
|
|
124
|
+
}
|
|
125
|
+
const existing = core.extensions.get(name)
|
|
126
|
+
if (existing) {
|
|
127
|
+
extensions.set(key, existing)
|
|
128
|
+
return existing
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const extension = core.registerExtension(name, {
|
|
59
132
|
encoding: 'utf8',
|
|
60
133
|
onmessage: (content, peer) => {
|
|
61
|
-
|
|
134
|
+
core.emit(EXTENSION_EVENT, name, content, peer)
|
|
62
135
|
}
|
|
63
136
|
})
|
|
64
137
|
|
|
138
|
+
extensions.set(key, extension)
|
|
139
|
+
|
|
65
140
|
return extension
|
|
66
141
|
}
|
|
67
142
|
|
|
68
|
-
function getExtensionPeers (
|
|
143
|
+
async function getExtensionPeers (core, name) {
|
|
69
144
|
// List peers with this extension
|
|
70
|
-
const allPeers =
|
|
145
|
+
const allPeers = core.peers
|
|
71
146
|
return allPeers.filter((peer) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!remoteExtensions) return false
|
|
75
|
-
|
|
76
|
-
const { names } = remoteExtensions
|
|
77
|
-
|
|
78
|
-
if (!names) return false
|
|
79
|
-
|
|
80
|
-
return names.includes(name)
|
|
147
|
+
return peer?.extensions?.has(name)
|
|
81
148
|
})
|
|
82
149
|
}
|
|
83
150
|
|
|
84
|
-
function listExtensionNames (
|
|
85
|
-
return
|
|
151
|
+
function listExtensionNames (core) {
|
|
152
|
+
return [...core.extensions.keys()]
|
|
86
153
|
}
|
|
87
154
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
155
|
+
if (extensionMessages) {
|
|
156
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`, async function listExtensions (request) {
|
|
157
|
+
const { hostname } = new URL(request.url)
|
|
158
|
+
const accept = request.headers.get('Accept') || ''
|
|
92
159
|
|
|
93
|
-
|
|
160
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
94
161
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
162
|
+
if (accept.includes('text/event-stream')) {
|
|
163
|
+
const events = new EventIterator(({ push }) => {
|
|
164
|
+
function onMessage (name, content, peer) {
|
|
165
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
166
|
+
// TODO: Fancy verification on the `name`?
|
|
167
|
+
// Send each line of content separately on a `data` line
|
|
168
|
+
const data = content.split('\n').map((line) => `data:${line}\n`).join('')
|
|
169
|
+
|
|
170
|
+
push(`id:${id}\nevent:${name}\n${data}\n`)
|
|
171
|
+
}
|
|
172
|
+
function onPeerOpen (peer) {
|
|
173
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
174
|
+
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
175
|
+
}
|
|
176
|
+
function onPeerRemove (peer) {
|
|
177
|
+
// Whatever, probably an uninitialized peer
|
|
178
|
+
if (!peer.remotePublicKey) return
|
|
179
|
+
const id = peer.remotePublicKey.toString('hex')
|
|
180
|
+
push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
|
|
181
|
+
}
|
|
182
|
+
core.on(EXTENSION_EVENT, onMessage)
|
|
183
|
+
core.on(PEER_OPEN, onPeerOpen)
|
|
184
|
+
core.on(PEER_REMOVE, onPeerRemove)
|
|
185
|
+
return () => {
|
|
186
|
+
core.removeListener(EXTENSION_EVENT, onMessage)
|
|
187
|
+
core.removeListener(PEER_OPEN, onPeerOpen)
|
|
188
|
+
core.removeListener(PEER_REMOVE, onPeerRemove)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
98
191
|
|
|
99
|
-
|
|
192
|
+
return {
|
|
193
|
+
statusCode: 200,
|
|
194
|
+
headers: {
|
|
195
|
+
[HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
|
|
196
|
+
},
|
|
197
|
+
body: events
|
|
198
|
+
}
|
|
199
|
+
}
|
|
100
200
|
|
|
101
|
-
|
|
201
|
+
const extensions = listExtensionNames(core)
|
|
202
|
+
return {
|
|
203
|
+
status: 200,
|
|
204
|
+
headers: { [HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON },
|
|
205
|
+
body: JSON.stringify(extensions, null, '\t')
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
router.get(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, async function listenExtension (request) {
|
|
209
|
+
const { hostname, pathname } = new URL(request.url)
|
|
210
|
+
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
102
211
|
|
|
103
|
-
|
|
212
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
104
213
|
|
|
105
|
-
|
|
106
|
-
responseHeaders['Access-Control-Allow-Origin'] = '*'
|
|
107
|
-
responseHeaders['Allow-CSP-From'] = '*'
|
|
108
|
-
responseHeaders['Access-Control-Allow-Headers'] = '*'
|
|
214
|
+
await getExtension(core, name)
|
|
109
215
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!path.startsWith('/')) path = '/' + path
|
|
216
|
+
const peers = await getExtensionPeers(core, name)
|
|
217
|
+
const finalPeers = formatPeers(peers)
|
|
218
|
+
const body = JSON.stringify(finalPeers, null, '\t')
|
|
114
219
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
220
|
+
return {
|
|
221
|
+
status: 200,
|
|
222
|
+
body,
|
|
223
|
+
headers: {
|
|
224
|
+
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
|
|
225
|
+
}
|
|
121
226
|
}
|
|
227
|
+
})
|
|
228
|
+
router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*`, async function broadcastExtension (request) {
|
|
229
|
+
const { hostname, pathname } = new URL(request.url)
|
|
230
|
+
const name = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
122
231
|
|
|
123
|
-
|
|
232
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
124
233
|
|
|
125
|
-
|
|
234
|
+
const extension = await getExtension(core, name)
|
|
235
|
+
const data = await request.text()
|
|
236
|
+
extension.broadcast(data)
|
|
237
|
+
|
|
238
|
+
return { status: 200 }
|
|
239
|
+
})
|
|
240
|
+
router.post(`hyper://*/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/*/*`, async function extensionToPeer (request) {
|
|
241
|
+
const { hostname, pathname } = new URL(request.url)
|
|
242
|
+
const subFolder = pathname.slice(`/${SPECIAL_FOLDER}/${EXTENSIONS_FOLDER_NAME}/`.length)
|
|
243
|
+
const [name, extensionPeer] = subFolder.split('/')
|
|
244
|
+
|
|
245
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
246
|
+
|
|
247
|
+
const extension = await getExtension(core, name)
|
|
248
|
+
const peers = await getExtensionPeers(core, name)
|
|
249
|
+
const peer = peers.find(({ remotePublicKey }) => remotePublicKey.toString('hex') === extensionPeer)
|
|
250
|
+
if (!peer) {
|
|
126
251
|
return {
|
|
127
|
-
|
|
128
|
-
headers:
|
|
129
|
-
|
|
252
|
+
status: 404,
|
|
253
|
+
headers: {
|
|
254
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
255
|
+
},
|
|
256
|
+
body: 'Peer Not Found'
|
|
130
257
|
}
|
|
131
258
|
}
|
|
259
|
+
const data = await request.arrayBuffer()
|
|
260
|
+
extension.send(data, peer)
|
|
261
|
+
return { status: 200 }
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (writable) {
|
|
266
|
+
router.get(`hyper://${SPECIAL_DOMAIN}/`, async function getKey (request) {
|
|
267
|
+
const key = new URL(request.url).searchParams.get('key')
|
|
268
|
+
if (!key) {
|
|
269
|
+
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
270
|
+
}
|
|
132
271
|
|
|
133
|
-
await
|
|
272
|
+
const drive = await getDriveFromKey(key, true)
|
|
134
273
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
else resolve()
|
|
146
|
-
})
|
|
147
|
-
})
|
|
274
|
+
return { body: drive.url }
|
|
275
|
+
})
|
|
276
|
+
router.post(`hyper://${SPECIAL_DOMAIN}/`, async function createKey (request) {
|
|
277
|
+
// TODO: Allow importing secret keys here
|
|
278
|
+
// Maybe specify a seed to use for generating the blobs?
|
|
279
|
+
// Else we'd need to specify the blobs keys and metadata keys
|
|
280
|
+
|
|
281
|
+
const key = new URL(request.url).searchParams.get('key')
|
|
282
|
+
if (!key) {
|
|
283
|
+
return { status: 400, body: 'Must specify key parameter to resolve' }
|
|
148
284
|
}
|
|
149
285
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
286
|
+
const drive = await getDriveFromKey(key, false)
|
|
287
|
+
|
|
288
|
+
return { body: drive.core.url }
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
router.put('hyper://*/**', async function putFiles (request) {
|
|
292
|
+
const { hostname, pathname } = new URL(request.url)
|
|
293
|
+
const contentType = request.headers.get('Content-Type') || ''
|
|
294
|
+
const isFormData = contentType.includes('multipart/form-data')
|
|
295
|
+
|
|
296
|
+
const drive = await getDrive(hostname)
|
|
297
|
+
|
|
298
|
+
if (isFormData) {
|
|
299
|
+
// It's a form! Get the files out and process them
|
|
300
|
+
const formData = await request.formData()
|
|
301
|
+
for (const [name, data] of formData) {
|
|
302
|
+
if (name !== 'file') continue
|
|
303
|
+
const filePath = posix.join(pathname, data.name)
|
|
304
|
+
await pipelinePromise(
|
|
305
|
+
Readable.from(data.stream()),
|
|
306
|
+
drive.createWriteStream(filePath, {
|
|
307
|
+
metadata: {
|
|
308
|
+
mtime: Date.now()
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
)
|
|
155
312
|
}
|
|
156
|
-
|
|
313
|
+
} else {
|
|
314
|
+
await pipelinePromise(
|
|
315
|
+
Readable.from(request.body),
|
|
316
|
+
drive.createWriteStream(pathname)
|
|
317
|
+
)
|
|
157
318
|
}
|
|
158
319
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const allowHeaders = isWritable ? ALL_ALLOW : READABLE_ALLOW
|
|
164
|
-
responseHeaders.Allow = allowHeaders.join(', ')
|
|
165
|
-
|
|
166
|
-
// We can say the file hasn't changed if the drive version hasn't changed
|
|
167
|
-
responseHeaders.ETag = `"${archive.version}"`
|
|
168
|
-
|
|
169
|
-
if (path.startsWith(SPECIAL_FOLDER)) {
|
|
170
|
-
if (path === SPECIAL_FOLDER) {
|
|
171
|
-
const files = [
|
|
172
|
-
TAGS_FOLDER_NAME,
|
|
173
|
-
EXTENSIONS_FOLDER_NAME
|
|
174
|
-
]
|
|
175
|
-
|
|
176
|
-
const data = renderFiles(headers, responseHeaders, url, path, files)
|
|
177
|
-
if (method === 'HEAD') {
|
|
178
|
-
return {
|
|
179
|
-
statusCode: 204,
|
|
180
|
-
headers: responseHeaders,
|
|
181
|
-
data: intoAsyncIterable('')
|
|
182
|
-
}
|
|
183
|
-
} else {
|
|
184
|
-
return {
|
|
185
|
-
statusCode: 200,
|
|
186
|
-
headers: responseHeaders,
|
|
187
|
-
data
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
} else if (path.startsWith(TAGS_FOLDER)) {
|
|
191
|
-
if (method === 'GET') {
|
|
192
|
-
if (path === TAGS_FOLDER) {
|
|
193
|
-
responseHeaders['x-is-directory'] = 'true'
|
|
194
|
-
const tags = await archive.getAllTags()
|
|
195
|
-
const tagsObject = Object.fromEntries(tags)
|
|
196
|
-
const json = JSON.stringify(tagsObject, null, '\t')
|
|
197
|
-
|
|
198
|
-
responseHeaders['Content-Type'] = 'application/json; charset=utf-8'
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
statusCode: 200,
|
|
202
|
-
headers: responseHeaders,
|
|
203
|
-
data: intoAsyncIterable(json)
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
const tagName = path.slice(TAGS_FOLDER.length)
|
|
207
|
-
try {
|
|
208
|
-
const tagVersion = await archive.getTaggedVersion(tagName)
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
statusCode: 200,
|
|
212
|
-
headers: responseHeaders,
|
|
213
|
-
data: intoAsyncIterable(`${tagVersion}`)
|
|
214
|
-
}
|
|
215
|
-
} catch {
|
|
216
|
-
return {
|
|
217
|
-
statusCode: 404,
|
|
218
|
-
headers: responseHeaders,
|
|
219
|
-
data: intoAsyncIterable('Tag Not Found')
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
} else if (method === 'DELETE') {
|
|
224
|
-
checkWritable(archive)
|
|
225
|
-
const tagName = path.slice(TAGS_FOLDER.length)
|
|
226
|
-
await archive.deleteTag(tagName || version)
|
|
227
|
-
responseHeaders.ETag = `"${archive.version}"`
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
statusCode: 200,
|
|
231
|
-
headers: responseHeaders,
|
|
232
|
-
data: intoAsyncIterable('')
|
|
233
|
-
}
|
|
234
|
-
} else if (method === 'PUT') {
|
|
235
|
-
checkWritable(archive)
|
|
236
|
-
const tagName = path.slice(TAGS_FOLDER.length)
|
|
237
|
-
const tagVersion = archive.version
|
|
238
|
-
|
|
239
|
-
await archive.createTag(tagName, tagVersion)
|
|
240
|
-
responseHeaders['Content-Type'] = 'text/plain; charset=utf-8'
|
|
241
|
-
responseHeaders.ETag = `"${archive.version}"`
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
statusCode: 200,
|
|
245
|
-
headers: responseHeaders,
|
|
246
|
-
data: intoAsyncIterable(`${tagVersion}`)
|
|
247
|
-
}
|
|
248
|
-
} else if (method === 'HEAD') {
|
|
249
|
-
return {
|
|
250
|
-
statusCode: 204,
|
|
251
|
-
headers: responseHeaders,
|
|
252
|
-
data: intoAsyncIterable('')
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
return {
|
|
256
|
-
statusCode: 405,
|
|
257
|
-
headers: responseHeaders,
|
|
258
|
-
data: intoAsyncIterable('Method Not Allowed')
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
} else if (path.startsWith(EXTENSIONS_FOLDER)) {
|
|
262
|
-
if (path === EXTENSIONS_FOLDER) {
|
|
263
|
-
if (method === 'GET') {
|
|
264
|
-
const accept = headers.get('Accept') || ''
|
|
265
|
-
if (!accept.includes('text/event-stream')) {
|
|
266
|
-
responseHeaders['x-is-directory'] = 'true'
|
|
267
|
-
|
|
268
|
-
const extensions = listExtensionNames(archive)
|
|
269
|
-
const data = renderFiles(headers, responseHeaders, url, path, extensions)
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
statusCode: 204,
|
|
273
|
-
headers: responseHeaders,
|
|
274
|
-
data
|
|
275
|
-
}
|
|
276
|
-
}
|
|
320
|
+
return { status: 201, headers: { Location: request.url } }
|
|
321
|
+
})
|
|
322
|
+
router.delete('hyper://*/**', async function putFiles (request) {
|
|
323
|
+
const { hostname, pathname } = new URL(request.url)
|
|
277
324
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
push(`id:${id}\nevent:${PEER_OPEN}\n\n`)
|
|
289
|
-
}
|
|
290
|
-
function onPeerRemove (peer) {
|
|
291
|
-
// Whatever, probably an uninitialized peer
|
|
292
|
-
if (!peer.remotePublicKey) return
|
|
293
|
-
const id = peer.remotePublicKey.toString('hex')
|
|
294
|
-
push(`id:${id}\nevent:${PEER_REMOVE}\n\n`)
|
|
295
|
-
}
|
|
296
|
-
archive.on(EXTENSION_EVENT, onMessage)
|
|
297
|
-
archive.on(PEER_OPEN, onPeerOpen)
|
|
298
|
-
archive.on(PEER_REMOVE, onPeerRemove)
|
|
299
|
-
return () => {
|
|
300
|
-
archive.removeListener(EXTENSION_EVENT, onMessage)
|
|
301
|
-
archive.removeListener(PEER_OPEN, onPeerOpen)
|
|
302
|
-
archive.removeListener(PEER_REMOVE, onPeerRemove)
|
|
303
|
-
}
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
responseHeaders['Content-Type'] = 'text/event-stream'
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
statusCode: 200,
|
|
310
|
-
headers: responseHeaders,
|
|
311
|
-
data: events
|
|
312
|
-
}
|
|
313
|
-
} else {
|
|
314
|
-
return {
|
|
315
|
-
statusCode: 405,
|
|
316
|
-
headers: responseHeaders,
|
|
317
|
-
data: intoAsyncIterable('Method Not Allowed')
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
} else {
|
|
321
|
-
let extensionName = path.slice(EXTENSIONS_FOLDER.length)
|
|
322
|
-
let extensionPeer = null
|
|
323
|
-
if (extensionName.includes('/')) {
|
|
324
|
-
const split = extensionName.split('/')
|
|
325
|
-
extensionName = split[0]
|
|
326
|
-
if (split[1]) extensionPeer = split[1]
|
|
327
|
-
}
|
|
328
|
-
if (method === 'POST') {
|
|
329
|
-
const extension = getExtension(archive, extensionName)
|
|
330
|
-
if (extensionPeer) {
|
|
331
|
-
const peers = getExtensionPeers(archive, extensionName)
|
|
332
|
-
const peer = peers.find(({ remotePublicKey }) => remotePublicKey.toString('hex') === extensionPeer)
|
|
333
|
-
if (!peer) {
|
|
334
|
-
return {
|
|
335
|
-
statusCode: 404,
|
|
336
|
-
headers: responseHeaders,
|
|
337
|
-
data: intoAsyncIterable('Peer Not Found')
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
extension.send(await collect(body), peer)
|
|
341
|
-
} else {
|
|
342
|
-
extension.broadcast(await collect(body))
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
statusCode: 200,
|
|
346
|
-
headers: responseHeaders,
|
|
347
|
-
data: intoAsyncIterable('')
|
|
348
|
-
}
|
|
349
|
-
} else if (method === 'GET') {
|
|
350
|
-
const accept = headers.get('Accept') || ''
|
|
351
|
-
if (!accept.includes('text/event-stream')) {
|
|
352
|
-
// Load up the extension into memory
|
|
353
|
-
getExtension(archive, extensionName)
|
|
354
|
-
|
|
355
|
-
const extensionPeers = getExtensionPeers(archive, extensionName)
|
|
356
|
-
const finalPeers = formatPeers(extensionPeers)
|
|
357
|
-
|
|
358
|
-
const json = JSON.stringify(finalPeers, null, '\t')
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
statusCode: 200,
|
|
362
|
-
header: responseHeaders,
|
|
363
|
-
data: intoAsyncIterable(json)
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
} else {
|
|
367
|
-
return {
|
|
368
|
-
statusCode: 405,
|
|
369
|
-
headers: responseHeaders,
|
|
370
|
-
data: intoAsyncIterable('Method Not Allowed')
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
return {
|
|
376
|
-
statusCode: 404,
|
|
377
|
-
headers: responseHeaders,
|
|
378
|
-
data: intoAsyncIterable('Not Found')
|
|
379
|
-
}
|
|
325
|
+
const drive = await getDrive(hostname)
|
|
326
|
+
|
|
327
|
+
if (pathname.endsWith('/')) {
|
|
328
|
+
let didDelete = false
|
|
329
|
+
for await (const entry of drive.list(pathname)) {
|
|
330
|
+
await drive.del(entry.key)
|
|
331
|
+
didDelete = true
|
|
332
|
+
}
|
|
333
|
+
if (!didDelete) {
|
|
334
|
+
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
380
335
|
}
|
|
336
|
+
return { status: 200 }
|
|
381
337
|
}
|
|
382
338
|
|
|
383
|
-
|
|
384
|
-
checkWritable(archive)
|
|
385
|
-
const contentType = headers.get('Content-Type') || headers.get('content-type')
|
|
386
|
-
const isFormData = contentType && contentType.includes('multipart/form-data')
|
|
387
|
-
|
|
388
|
-
if (path.endsWith('/')) {
|
|
389
|
-
await makeDir(path, { fs: archive })
|
|
390
|
-
const busboy = new Busboy({ headers: rawHeaders })
|
|
391
|
-
|
|
392
|
-
const toUpload = new EventIterator(({ push, stop, fail }) => {
|
|
393
|
-
busboy.once('error', fail)
|
|
394
|
-
busboy.once('finish', stop)
|
|
395
|
-
|
|
396
|
-
busboy.on('file', async (fieldName, fileData, fileName) => {
|
|
397
|
-
const finalPath = posixPath.join(path, fileName)
|
|
398
|
-
|
|
399
|
-
const source = Readable.from(fileData)
|
|
400
|
-
const destination = archive.createWriteStream(finalPath)
|
|
401
|
-
|
|
402
|
-
source.pipe(destination)
|
|
403
|
-
try {
|
|
404
|
-
Promise.race([
|
|
405
|
-
once(source, 'error').then((e) => { throw e }),
|
|
406
|
-
once(destination, 'error').then((e) => { throw e }),
|
|
407
|
-
once(source, 'end')
|
|
408
|
-
])
|
|
409
|
-
} catch (e) {
|
|
410
|
-
fail(e)
|
|
411
|
-
}
|
|
412
|
-
})
|
|
339
|
+
const entry = await drive.entry(pathname)
|
|
413
340
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
341
|
+
if (!entry) {
|
|
342
|
+
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
343
|
+
}
|
|
344
|
+
await drive.del(pathname)
|
|
417
345
|
|
|
418
|
-
|
|
346
|
+
return { status: 200 }
|
|
347
|
+
})
|
|
348
|
+
}
|
|
419
349
|
|
|
420
|
-
|
|
350
|
+
router.head('hyper://*/**', async function headFiles (request) {
|
|
351
|
+
const url = new URL(request.url)
|
|
352
|
+
const { hostname, pathname, searchParams } = url
|
|
353
|
+
const accept = request.headers.get('Accept') || ''
|
|
354
|
+
const isRanged = request.headers.get('Range') || ''
|
|
355
|
+
const noResolve = searchParams.has('noResolve')
|
|
356
|
+
const isDirectory = pathname.endsWith('/')
|
|
421
357
|
|
|
422
|
-
|
|
423
|
-
statusCode: 200,
|
|
424
|
-
headers: responseHeaders,
|
|
425
|
-
data: intoAsyncIterable(canonical)
|
|
426
|
-
}
|
|
427
|
-
} else {
|
|
428
|
-
if (isFormData) {
|
|
429
|
-
return {
|
|
430
|
-
statusCode: 400,
|
|
431
|
-
headers: responseHeaders,
|
|
432
|
-
data: intoAsyncIterable('FormData only supported for folders (ending with a /)')
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
const parentDir = path.split('/').slice(0, -1).join('/')
|
|
436
|
-
if (parentDir) {
|
|
437
|
-
await makeDir(parentDir, { fs: archive })
|
|
438
|
-
}
|
|
358
|
+
const drive = await getDrive(hostname)
|
|
439
359
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
360
|
+
const resHeaders = {
|
|
361
|
+
ETag: `${drive.version}`,
|
|
362
|
+
'Accept-Ranges': 'bytes',
|
|
363
|
+
Link: `<${drive.core.url}>; rel="canonical"`
|
|
364
|
+
}
|
|
443
365
|
|
|
444
|
-
|
|
366
|
+
if (isDirectory) {
|
|
367
|
+
const entries = await listEntries(drive, pathname)
|
|
445
368
|
|
|
446
|
-
|
|
447
|
-
once(source, 'error'),
|
|
448
|
-
once(destination, 'error'),
|
|
449
|
-
once(source, 'end')
|
|
450
|
-
])
|
|
451
|
-
}
|
|
452
|
-
responseHeaders.ETag = `"${archive.version}"`
|
|
369
|
+
const hasItems = entries.length
|
|
453
370
|
|
|
371
|
+
if (!hasItems && pathname !== '/') {
|
|
454
372
|
return {
|
|
455
|
-
|
|
456
|
-
headers:
|
|
457
|
-
|
|
373
|
+
status: 404,
|
|
374
|
+
headers: {
|
|
375
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
376
|
+
}
|
|
458
377
|
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!noResolve) {
|
|
381
|
+
if (entries.includes('index.html')) {
|
|
462
382
|
return {
|
|
463
|
-
|
|
464
|
-
headers:
|
|
465
|
-
|
|
383
|
+
status: 204,
|
|
384
|
+
headers: {
|
|
385
|
+
...resHeaders,
|
|
386
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_HTML
|
|
387
|
+
}
|
|
466
388
|
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const stats = await archive.stat(path)
|
|
471
|
-
// Weird stuff happening up in here...
|
|
472
|
-
const stat = Array.isArray(stats) ? stats[0] : stats
|
|
389
|
+
}
|
|
390
|
+
}
|
|
473
391
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
392
|
+
// TODO: Add range header calculation
|
|
393
|
+
if (accept.includes('text/html')) {
|
|
394
|
+
return {
|
|
395
|
+
status: 204,
|
|
396
|
+
headers: {
|
|
397
|
+
...resHeaders,
|
|
398
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_HTML
|
|
478
399
|
}
|
|
479
|
-
|
|
400
|
+
}
|
|
401
|
+
}
|
|
480
402
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
403
|
+
return {
|
|
404
|
+
status: 204,
|
|
405
|
+
headers: {
|
|
406
|
+
...resHeaders,
|
|
407
|
+
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
|
|
486
408
|
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const contentFeed = await archive.getContent()
|
|
490
|
-
const events = new EventIterator(({ push, fail }) => {
|
|
491
|
-
const watcher = archive.watch(path, () => {
|
|
492
|
-
const event = 'change'
|
|
493
|
-
const data = archive.version
|
|
494
|
-
push({ event, data })
|
|
495
|
-
})
|
|
496
|
-
watcher.on('error', fail)
|
|
497
|
-
function onDownloadMetadata (index) {
|
|
498
|
-
const event = 'download'
|
|
499
|
-
const source = archive.metadata.key.toString('hex')
|
|
500
|
-
const data = { index, source }
|
|
501
|
-
push({ event, data })
|
|
502
|
-
}
|
|
503
|
-
function onUploadMetadata (index) {
|
|
504
|
-
const event = 'download'
|
|
505
|
-
const source = archive.metadata.key.toString('hex')
|
|
506
|
-
const data = { index, source }
|
|
507
|
-
push({ event, data })
|
|
508
|
-
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
509
411
|
|
|
510
|
-
|
|
511
|
-
const event = 'download'
|
|
512
|
-
const source = contentFeed.key.toString('hex')
|
|
513
|
-
const data = { index, source }
|
|
514
|
-
push({ event, data })
|
|
515
|
-
}
|
|
516
|
-
function onUploadContent (index) {
|
|
517
|
-
const event = 'download'
|
|
518
|
-
const source = contentFeed.key.toString('hex')
|
|
519
|
-
const data = { index, source }
|
|
520
|
-
push({ event, data })
|
|
521
|
-
}
|
|
412
|
+
const { entry, path } = await resolvePath(drive, pathname, noResolve)
|
|
522
413
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
archive.metadata.on('upload', onUploadMetadata)
|
|
527
|
-
contentFeed.on('download', onDownloadContent)
|
|
528
|
-
contentFeed.on('upload', onUploadMetadata)
|
|
529
|
-
return () => {
|
|
530
|
-
watcher.destroy()
|
|
531
|
-
archive.metadata.removeListener('download', onDownloadMetadata)
|
|
532
|
-
archive.metadata.removeListener('upload', onUploadMetadata)
|
|
533
|
-
contentFeed.removeListener('download', onDownloadContent)
|
|
534
|
-
contentFeed.removeListener('upload', onUploadContent)
|
|
535
|
-
}
|
|
536
|
-
})
|
|
537
|
-
async function * startReader () {
|
|
538
|
-
for await (const { event, data } of events) {
|
|
539
|
-
yield `event:${event}\ndata:${JSON.stringify(data)}\n\n`
|
|
540
|
-
}
|
|
541
|
-
}
|
|
414
|
+
if (!entry) {
|
|
415
|
+
return { status: 404, body: 'Not Found' }
|
|
416
|
+
}
|
|
542
417
|
|
|
543
|
-
|
|
418
|
+
resHeaders.ETag = `${entry.seq}`
|
|
544
419
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
headers: responseHeaders,
|
|
548
|
-
data: startReader()
|
|
549
|
-
}
|
|
550
|
-
}
|
|
420
|
+
const contentType = getMimeType(path)
|
|
421
|
+
resHeaders['Content-Type'] = contentType
|
|
551
422
|
|
|
552
|
-
|
|
553
|
-
|
|
423
|
+
if (entry.metadata?.mtime) {
|
|
424
|
+
const date = new Date(entry.metadata.mtime)
|
|
425
|
+
resHeaders['Last-Modified'] = date.toUTCString()
|
|
426
|
+
}
|
|
554
427
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
428
|
+
const size = entry.value.byteLength
|
|
429
|
+
if (isRanged) {
|
|
430
|
+
const ranges = parseRange(size, isRanged)
|
|
558
431
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const entry = `dat://${key.toString('hex')}\nttl=3600`
|
|
563
|
-
return {
|
|
564
|
-
statusCode: 200,
|
|
565
|
-
headers: responseHeaders,
|
|
566
|
-
data: intoAsyncIterable(entry)
|
|
567
|
-
}
|
|
568
|
-
}
|
|
432
|
+
if (ranges && ranges.length && ranges.type === 'bytes') {
|
|
433
|
+
const [{ start, end }] = ranges
|
|
434
|
+
const length = (end - start + 1)
|
|
569
435
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
headers: responseHeaders,
|
|
577
|
-
data: intoAsyncIterable(entry)
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
try {
|
|
581
|
-
if (searchParams.has('noResolve')) {
|
|
582
|
-
const stats = await archive.stat(path)
|
|
583
|
-
stat = stats[0]
|
|
584
|
-
} else {
|
|
585
|
-
const resolved = await resolveDatPath(archive, path)
|
|
586
|
-
finalPath = resolved.path
|
|
587
|
-
stat = resolved.stat
|
|
588
|
-
}
|
|
589
|
-
} catch (e) {
|
|
590
|
-
responseHeaders['Content-Type'] = 'text/plain; charset=utf-8'
|
|
591
|
-
return {
|
|
592
|
-
statusCode: 404,
|
|
593
|
-
headers: responseHeaders,
|
|
594
|
-
data: intoAsyncIterable(e.stack)
|
|
436
|
+
return {
|
|
437
|
+
status: 200,
|
|
438
|
+
headers: {
|
|
439
|
+
...resHeaders,
|
|
440
|
+
'Content-Length': `${length}`,
|
|
441
|
+
'Content-Range': `bytes ${start}-${end}/${size}`
|
|
595
442
|
}
|
|
596
443
|
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
597
446
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
let statusCode = 200
|
|
447
|
+
return {
|
|
448
|
+
status: 200,
|
|
449
|
+
headers: resHeaders
|
|
450
|
+
}
|
|
451
|
+
})
|
|
604
452
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
453
|
+
// TODO: Redirect on directories without trailing slash
|
|
454
|
+
router.get('hyper://*/**', async function getFiles (request) {
|
|
455
|
+
const url = new URL(request.url)
|
|
456
|
+
const { hostname, pathname, searchParams } = url
|
|
457
|
+
const accept = request.headers.get('Accept') || ''
|
|
458
|
+
const noResolve = searchParams.has('noResolve')
|
|
459
|
+
const isDirectory = pathname.endsWith('/')
|
|
609
460
|
|
|
610
|
-
|
|
611
|
-
if (finalPath === '/') files.unshift('$/')
|
|
461
|
+
const drive = await getDrive(hostname)
|
|
612
462
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
463
|
+
if (isDirectory) {
|
|
464
|
+
const resHeaders = {
|
|
465
|
+
ETag: `${drive.version}`,
|
|
466
|
+
Link: `<${drive.core.url}>; rel="canonical"`
|
|
467
|
+
}
|
|
616
468
|
|
|
617
|
-
|
|
618
|
-
const { blocks, downloadedBlocks } = await archive.stats(finalPath)
|
|
619
|
-
responseHeaders['X-Blocks'] = `${blocks}`
|
|
620
|
-
responseHeaders['X-Blocks-Downloaded'] = `${downloadedBlocks}`
|
|
621
|
-
} catch (e) {
|
|
622
|
-
// Don't worry about it, it's optional.
|
|
623
|
-
}
|
|
469
|
+
const entries = await listEntries(drive, pathname)
|
|
624
470
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const [{ start, end }] = ranges
|
|
633
|
-
const length = (end - start + 1)
|
|
634
|
-
responseHeaders['Content-Length'] = `${length}`
|
|
635
|
-
responseHeaders['Content-Range'] = `bytes ${start}-${end}/${size}`
|
|
636
|
-
if (method !== 'HEAD') {
|
|
637
|
-
data = archive.createReadStream(finalPath, {
|
|
638
|
-
start,
|
|
639
|
-
end
|
|
640
|
-
})
|
|
641
|
-
}
|
|
642
|
-
} else {
|
|
643
|
-
if (method !== 'HEAD') {
|
|
644
|
-
data = archive.createReadStream(finalPath)
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
} else if (method !== 'HEAD') {
|
|
648
|
-
data = archive.createReadStream(finalPath)
|
|
471
|
+
if (!entries.length && pathname !== '/') {
|
|
472
|
+
return {
|
|
473
|
+
status: 404,
|
|
474
|
+
body: '[]',
|
|
475
|
+
headers: {
|
|
476
|
+
...resHeaders,
|
|
477
|
+
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
|
|
649
478
|
}
|
|
650
479
|
}
|
|
480
|
+
}
|
|
651
481
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
headers: responseHeaders,
|
|
656
|
-
data: intoAsyncIterable('')
|
|
657
|
-
}
|
|
658
|
-
} else {
|
|
659
|
-
return {
|
|
660
|
-
statusCode,
|
|
661
|
-
headers: responseHeaders,
|
|
662
|
-
data
|
|
663
|
-
}
|
|
482
|
+
if (!noResolve) {
|
|
483
|
+
if (entries.includes('index.html')) {
|
|
484
|
+
return serveFile(request.headers, drive, posix.join(pathname, 'index.html'))
|
|
664
485
|
}
|
|
665
|
-
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (accept.includes('text/html')) {
|
|
489
|
+
const body = await renderIndex(url, entries, fetch)
|
|
666
490
|
return {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
491
|
+
status: 200,
|
|
492
|
+
body,
|
|
493
|
+
headers: {
|
|
494
|
+
...resHeaders,
|
|
495
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_HTML
|
|
496
|
+
}
|
|
670
497
|
}
|
|
671
498
|
}
|
|
672
|
-
|
|
673
|
-
const isUnauthorized = (e.message === NOT_WRITABLE_ERROR)
|
|
674
|
-
const statusCode = isUnauthorized ? 403 : 500
|
|
675
|
-
const statusText = isUnauthorized ? 'Not Authorized' : 'Server Error'
|
|
499
|
+
|
|
676
500
|
return {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
headers:
|
|
680
|
-
|
|
501
|
+
status: 200,
|
|
502
|
+
body: JSON.stringify(entries, null, '\t'),
|
|
503
|
+
headers: {
|
|
504
|
+
...resHeaders,
|
|
505
|
+
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
|
|
506
|
+
}
|
|
681
507
|
}
|
|
682
508
|
}
|
|
683
|
-
}
|
|
684
509
|
|
|
685
|
-
|
|
686
|
-
if (Hyperdrive) return Hyperdrive
|
|
687
|
-
return getSDK().then(({ Hyperdrive }) => Hyperdrive)
|
|
688
|
-
}
|
|
510
|
+
const { entry, path } = await resolvePath(drive, pathname, noResolve)
|
|
689
511
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
gettingSDK = SDK(opts).then((gotSDK) => {
|
|
694
|
-
sdk = gotSDK
|
|
695
|
-
gettingSDK = null
|
|
696
|
-
onClose = async () => sdk.close()
|
|
697
|
-
Hyperdrive = sdk.Hyperdrive
|
|
512
|
+
if (!entry) {
|
|
513
|
+
return { status: 404, body: 'Not Found' }
|
|
514
|
+
}
|
|
698
515
|
|
|
699
|
-
|
|
700
|
-
|
|
516
|
+
return serveFile(request.headers, drive, path)
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
return fetch
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function serveFile (headers, drive, pathname) {
|
|
523
|
+
const isRanged = headers.get('Range') || ''
|
|
524
|
+
const contentType = getMimeType(pathname)
|
|
701
525
|
|
|
702
|
-
|
|
526
|
+
const entry = await drive.entry(pathname)
|
|
527
|
+
|
|
528
|
+
const resHeaders = {
|
|
529
|
+
ETag: `${entry.seq}`,
|
|
530
|
+
[HEADER_CONTENT_TYPE]: contentType,
|
|
531
|
+
'Accept-Ranges': 'bytes',
|
|
532
|
+
Link: `<${drive.core.url}>; rel="canonical"`
|
|
703
533
|
}
|
|
704
534
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
throw new Error(NOT_WRITABLE_ERROR)
|
|
709
|
-
}
|
|
535
|
+
if (entry.metadata?.mtime) {
|
|
536
|
+
const date = new Date(entry.metadata.mtime)
|
|
537
|
+
resHeaders['Last-Modified'] = date.toUTCString()
|
|
710
538
|
}
|
|
711
|
-
}
|
|
712
539
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
let version = null
|
|
717
|
-
if (key.includes('+')) [key, version] = key.split('+')
|
|
540
|
+
const size = entry.value.blob.byteLength
|
|
541
|
+
if (isRanged) {
|
|
542
|
+
const ranges = parseRange(size, isRanged)
|
|
718
543
|
|
|
719
|
-
|
|
720
|
-
|
|
544
|
+
if (ranges && ranges.length && ranges.type === 'bytes') {
|
|
545
|
+
const [{ start, end }] = ranges
|
|
546
|
+
const length = (end - start + 1)
|
|
721
547
|
|
|
722
|
-
|
|
548
|
+
return {
|
|
549
|
+
status: 200,
|
|
550
|
+
body: drive.createReadStream(pathname, {
|
|
551
|
+
start,
|
|
552
|
+
end
|
|
553
|
+
}),
|
|
554
|
+
headers: {
|
|
555
|
+
...resHeaders,
|
|
556
|
+
'Content-Length': `${length}`,
|
|
557
|
+
'Content-Range': `bytes ${start}-${end}/${size}`
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
status: 200,
|
|
564
|
+
headers: {
|
|
565
|
+
...resHeaders,
|
|
566
|
+
'Content-Length': `${size}`
|
|
567
|
+
},
|
|
568
|
+
body: drive.createReadStream(pathname)
|
|
569
|
+
}
|
|
723
570
|
}
|
|
724
571
|
|
|
725
|
-
|
|
726
|
-
|
|
572
|
+
function makeToTry (pathname) {
|
|
573
|
+
return [
|
|
574
|
+
pathname,
|
|
575
|
+
pathname + '.html',
|
|
576
|
+
pathname + '.md'
|
|
577
|
+
]
|
|
727
578
|
}
|
|
728
579
|
|
|
729
|
-
function
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
return mimeType
|
|
733
|
-
}
|
|
580
|
+
async function resolvePath (drive, pathname, noResolve) {
|
|
581
|
+
if (noResolve) {
|
|
582
|
+
const entry = drive.entry(pathname)
|
|
734
583
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
<title>${url}</title>
|
|
738
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
739
|
-
<h1>Index of ${path}</h1>
|
|
740
|
-
<ul>
|
|
741
|
-
<li><a href="../">../</a></li>${files.map((file) => `
|
|
742
|
-
<li><a href="${file}">./${file}</a></li>
|
|
743
|
-
`).join('')}
|
|
744
|
-
</ul>
|
|
745
|
-
`
|
|
746
|
-
}
|
|
584
|
+
return { entry, path: pathname }
|
|
585
|
+
}
|
|
747
586
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
} else {
|
|
754
|
-
const json = JSON.stringify(files, null, '\t')
|
|
755
|
-
responseHeaders['Content-Type'] = 'application/json; charset=utf-8'
|
|
756
|
-
return intoAsyncIterable(json)
|
|
587
|
+
for (const path of makeToTry(pathname)) {
|
|
588
|
+
const entry = await drive.entry(path)
|
|
589
|
+
if (entry) {
|
|
590
|
+
return { entry, path }
|
|
591
|
+
}
|
|
757
592
|
}
|
|
758
|
-
}
|
|
759
593
|
|
|
760
|
-
|
|
761
|
-
return new Promise((resolve, reject) => {
|
|
762
|
-
const isError = name === 'error'
|
|
763
|
-
const cb = isError ? reject : resolve
|
|
764
|
-
ee.once(name, cb)
|
|
765
|
-
})
|
|
594
|
+
return { entry: null, path: null }
|
|
766
595
|
}
|
|
767
596
|
|
|
768
|
-
async function
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
597
|
+
async function listEntries (drive, pathname = '/') {
|
|
598
|
+
const entries = []
|
|
599
|
+
for await (const path of drive.readdir(pathname)) {
|
|
600
|
+
const stat = await drive.entry(path)
|
|
601
|
+
if (stat === null) {
|
|
602
|
+
entries.push(path + '/')
|
|
603
|
+
} else {
|
|
604
|
+
entries.push(path)
|
|
605
|
+
}
|
|
773
606
|
}
|
|
774
|
-
|
|
775
|
-
return buffer
|
|
607
|
+
return entries
|
|
776
608
|
}
|
|
777
609
|
|
|
778
610
|
function formatPeers (peers) {
|
|
779
|
-
return peers.map((
|
|
611
|
+
return peers.map((peer) => {
|
|
612
|
+
const remotePublicKey = peer.remotePublicKey.toString('hex')
|
|
613
|
+
const remoteHost = peer.stream?.rawStream?.remoteHost
|
|
780
614
|
return {
|
|
781
|
-
remotePublicKey
|
|
782
|
-
|
|
783
|
-
remoteAddress,
|
|
784
|
-
stats
|
|
615
|
+
remotePublicKey,
|
|
616
|
+
remoteHost
|
|
785
617
|
}
|
|
786
618
|
})
|
|
787
619
|
}
|
|
620
|
+
|
|
621
|
+
function getMimeType (path) {
|
|
622
|
+
let mimeType = mime.getType(path) || 'text/plain; charset=utf-8'
|
|
623
|
+
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
|
|
624
|
+
return mimeType
|
|
625
|
+
}
|