hypercore-fetch 8.6.1 → 8.6.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/.github/workflows/test.yml +0 -0
- package/LICENSE +0 -0
- package/README.md +67 -116
- package/index.js +482 -670
- package/package.json +8 -17
- package/test.js +245 -186
- package/bin.js +0 -27
package/index.js
CHANGED
|
@@ -1,787 +1,599 @@
|
|
|
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'
|
|
25
|
+
|
|
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
|
+
}
|
|
38
|
+
|
|
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
|
+
}
|
|
34
64
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
|
78
|
+
|
|
79
|
+
return dbCore
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getDrive (hostname) {
|
|
83
|
+
if (drives.has(hostname)) {
|
|
84
|
+
return drives.get(hostname)
|
|
85
|
+
}
|
|
86
|
+
|
|
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
|
+
}
|
|
43
108
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let onClose = async () => undefined
|
|
109
|
+
const corestore = sdk.namespace(key)
|
|
110
|
+
const drive = new Hyperdrive(corestore)
|
|
47
111
|
|
|
48
|
-
|
|
112
|
+
await drive.ready()
|
|
49
113
|
|
|
50
|
-
|
|
114
|
+
drives.set(key, drive)
|
|
115
|
+
drives.set(drive.core.id, drive)
|
|
51
116
|
|
|
52
|
-
|
|
117
|
+
return drive
|
|
118
|
+
}
|
|
53
119
|
|
|
54
|
-
function getExtension (
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
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
|
+
}
|
|
57
130
|
|
|
58
|
-
const extension =
|
|
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('')
|
|
98
169
|
|
|
99
|
-
|
|
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
|
+
})
|
|
100
191
|
|
|
101
|
-
|
|
192
|
+
return {
|
|
193
|
+
statusCode: 200,
|
|
194
|
+
headers: {
|
|
195
|
+
[HEADER_CONTENT_TYPE]: MIME_EVENT_STREAM
|
|
196
|
+
},
|
|
197
|
+
body: events
|
|
198
|
+
}
|
|
199
|
+
}
|
|
102
200
|
|
|
103
|
-
|
|
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)
|
|
211
|
+
|
|
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)
|
|
231
|
+
|
|
232
|
+
const core = await getCore(`hyper://${hostname}/`)
|
|
122
233
|
|
|
123
|
-
|
|
234
|
+
const extension = await getExtension(core, name)
|
|
235
|
+
const data = await request.text()
|
|
236
|
+
extension.broadcast(data)
|
|
124
237
|
|
|
125
|
-
|
|
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
|
+
}
|
|
132
264
|
|
|
133
|
-
|
|
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
|
+
}
|
|
134
271
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
})
|
|
272
|
+
const drive = await getDriveFromKey(key, true)
|
|
273
|
+
|
|
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
|
-
|
|
163
|
-
const
|
|
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
|
+
// TODO: Use 201 with location in response headers
|
|
321
|
+
return { status: 200 }
|
|
322
|
+
})
|
|
323
|
+
router.delete('hyper://*/**', async function putFiles (request) {
|
|
324
|
+
const { hostname, pathname } = new URL(request.url)
|
|
277
325
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
function onPeerOpen (peer) {
|
|
287
|
-
const id = peer.remotePublicKey.toString('hex')
|
|
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
|
-
}
|
|
326
|
+
const drive = await getDrive(hostname)
|
|
327
|
+
|
|
328
|
+
if (pathname.endsWith('/')) {
|
|
329
|
+
let didDelete = false
|
|
330
|
+
for await (const entry of drive.list(pathname)) {
|
|
331
|
+
await drive.del(entry.key)
|
|
332
|
+
didDelete = true
|
|
380
333
|
}
|
|
334
|
+
if (!didDelete) {
|
|
335
|
+
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
336
|
+
}
|
|
337
|
+
return { status: 200 }
|
|
381
338
|
}
|
|
382
339
|
|
|
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
|
-
})
|
|
340
|
+
const entry = await drive.entry(pathname)
|
|
413
341
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
342
|
+
if (!entry) {
|
|
343
|
+
return { status: 404, body: 'Not Found', headers: { [HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN } }
|
|
344
|
+
}
|
|
345
|
+
await drive.del(pathname)
|
|
417
346
|
|
|
418
|
-
|
|
347
|
+
return { status: 200 }
|
|
348
|
+
})
|
|
349
|
+
}
|
|
419
350
|
|
|
420
|
-
|
|
351
|
+
router.head('hyper://*/**', async function headFiles (request) {
|
|
352
|
+
const url = new URL(request.url)
|
|
353
|
+
const { hostname, pathname, searchParams } = url
|
|
354
|
+
const accept = request.headers.get('Accept') || ''
|
|
355
|
+
const isRanged = request.headers.get('Range') || ''
|
|
356
|
+
const noResolve = searchParams.has('noResolve')
|
|
357
|
+
const isDirectory = pathname.endsWith('/')
|
|
421
358
|
|
|
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
|
-
}
|
|
359
|
+
const drive = await getDrive(hostname)
|
|
439
360
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
361
|
+
const resHeaders = {
|
|
362
|
+
ETag: `${drive.version}`,
|
|
363
|
+
'Accept-Ranges': 'bytes',
|
|
364
|
+
Link: `<${drive.core.url}>; rel="canonical"`
|
|
365
|
+
}
|
|
443
366
|
|
|
444
|
-
|
|
367
|
+
if (isDirectory) {
|
|
368
|
+
const entries = await listEntries(drive, pathname)
|
|
445
369
|
|
|
446
|
-
|
|
447
|
-
once(source, 'error'),
|
|
448
|
-
once(destination, 'error'),
|
|
449
|
-
once(source, 'end')
|
|
450
|
-
])
|
|
451
|
-
}
|
|
452
|
-
responseHeaders.ETag = `"${archive.version}"`
|
|
370
|
+
const hasItems = entries.length
|
|
453
371
|
|
|
372
|
+
if (!hasItems && pathname !== '/') {
|
|
454
373
|
return {
|
|
455
|
-
|
|
456
|
-
headers:
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
} else if (method === 'DELETE') {
|
|
460
|
-
if (headers.get('x-clear') === 'cache') {
|
|
461
|
-
await archive.clear(path)
|
|
462
|
-
return {
|
|
463
|
-
statusCode: 200,
|
|
464
|
-
headers: responseHeaders,
|
|
465
|
-
data: intoAsyncIterable('')
|
|
374
|
+
status: 404,
|
|
375
|
+
headers: {
|
|
376
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_PLAIN
|
|
466
377
|
}
|
|
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
|
|
378
|
+
}
|
|
379
|
+
}
|
|
473
380
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
381
|
+
if (!noResolve) {
|
|
382
|
+
if (entries.includes('index.html')) {
|
|
383
|
+
return {
|
|
384
|
+
status: 204,
|
|
385
|
+
headers: {
|
|
386
|
+
...resHeaders,
|
|
387
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_HTML
|
|
388
|
+
}
|
|
478
389
|
}
|
|
479
|
-
|
|
390
|
+
}
|
|
391
|
+
}
|
|
480
392
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
393
|
+
// TODO: Add range header calculation
|
|
394
|
+
if (accept.includes('text/html')) {
|
|
395
|
+
return {
|
|
396
|
+
status: 204,
|
|
397
|
+
headers: {
|
|
398
|
+
...resHeaders,
|
|
399
|
+
[HEADER_CONTENT_TYPE]: MIME_TEXT_HTML
|
|
485
400
|
}
|
|
486
401
|
}
|
|
487
|
-
}
|
|
488
|
-
if (method === 'GET' && headers.get('Accept') === 'text/event-stream') {
|
|
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
|
-
}
|
|
402
|
+
}
|
|
509
403
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const data = { index, source }
|
|
520
|
-
push({ event, data })
|
|
521
|
-
}
|
|
404
|
+
return {
|
|
405
|
+
status: 204,
|
|
406
|
+
headers: {
|
|
407
|
+
...resHeaders,
|
|
408
|
+
[HEADER_CONTENT_TYPE]: MIME_APPLICATION_JSON
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const entry = await drive.entry(pathname)
|
|
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(pathname)
|
|
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 = 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
|
-
|
|
685
|
-
function getHyperdrive () {
|
|
686
|
-
if (Hyperdrive) return Hyperdrive
|
|
687
|
-
return getSDK().then(({ Hyperdrive }) => Hyperdrive)
|
|
688
|
-
}
|
|
509
|
+
const entry = await drive.entry(pathname)
|
|
689
510
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (gettingSDK) return gettingSDK
|
|
693
|
-
gettingSDK = SDK(opts).then((gotSDK) => {
|
|
694
|
-
sdk = gotSDK
|
|
695
|
-
gettingSDK = null
|
|
696
|
-
onClose = async () => sdk.close()
|
|
697
|
-
Hyperdrive = sdk.Hyperdrive
|
|
698
|
-
|
|
699
|
-
return sdk
|
|
700
|
-
})
|
|
701
|
-
|
|
702
|
-
return gettingSDK
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function checkWritable (archive) {
|
|
706
|
-
if (!writable) throw new Error(NOT_WRITABLE_ERROR)
|
|
707
|
-
if (!archive.writable) {
|
|
708
|
-
throw new Error(NOT_WRITABLE_ERROR)
|
|
511
|
+
if (!entry) {
|
|
512
|
+
return { status: 404, body: 'Not Found' }
|
|
709
513
|
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
514
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
let key = parsed.hostname
|
|
716
|
-
let version = null
|
|
717
|
-
if (key.includes('+')) [key, version] = key.split('+')
|
|
718
|
-
|
|
719
|
-
parsed.key = key
|
|
720
|
-
parsed.version = version
|
|
515
|
+
return serveFile(request.headers, drive, pathname)
|
|
516
|
+
})
|
|
721
517
|
|
|
722
|
-
return
|
|
518
|
+
return fetch
|
|
723
519
|
}
|
|
724
520
|
|
|
725
|
-
async function
|
|
726
|
-
|
|
727
|
-
|
|
521
|
+
async function serveFile (headers, drive, pathname) {
|
|
522
|
+
const isRanged = headers.get('Range') || ''
|
|
523
|
+
const contentType = getMimeType(pathname)
|
|
728
524
|
|
|
729
|
-
|
|
730
|
-
let mimeType = mime.getType(path) || 'text/plain; charset=utf-8'
|
|
731
|
-
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
|
|
732
|
-
return mimeType
|
|
733
|
-
}
|
|
525
|
+
const entry = await drive.entry(pathname)
|
|
734
526
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
<li><a href="../">../</a></li>${files.map((file) => `
|
|
742
|
-
<li><a href="${file}">./${file}</a></li>
|
|
743
|
-
`).join('')}
|
|
744
|
-
</ul>
|
|
745
|
-
`
|
|
746
|
-
}
|
|
527
|
+
const resHeaders = {
|
|
528
|
+
ETag: `${entry.seq}`,
|
|
529
|
+
[HEADER_CONTENT_TYPE]: contentType,
|
|
530
|
+
'Accept-Ranges': 'bytes',
|
|
531
|
+
Link: `<${drive.core.url}>; rel="canonical"`
|
|
532
|
+
}
|
|
747
533
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
responseHeaders['Content-Type'] = 'text/html; charset=utf-8'
|
|
752
|
-
return intoAsyncIterable(page)
|
|
753
|
-
} else {
|
|
754
|
-
const json = JSON.stringify(files, null, '\t')
|
|
755
|
-
responseHeaders['Content-Type'] = 'application/json; charset=utf-8'
|
|
756
|
-
return intoAsyncIterable(json)
|
|
534
|
+
if (entry.metadata?.mtime) {
|
|
535
|
+
const date = new Date(entry.metadata.mtime)
|
|
536
|
+
resHeaders['Last-Modified'] = date.toUTCString()
|
|
757
537
|
}
|
|
758
|
-
}
|
|
759
538
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
const cb = isError ? reject : resolve
|
|
764
|
-
ee.once(name, cb)
|
|
765
|
-
})
|
|
766
|
-
}
|
|
539
|
+
const size = entry.value.blob.byteLength
|
|
540
|
+
if (isRanged) {
|
|
541
|
+
const ranges = parseRange(size, isRanged)
|
|
767
542
|
|
|
768
|
-
|
|
769
|
-
|
|
543
|
+
if (ranges && ranges.length && ranges.type === 'bytes') {
|
|
544
|
+
const [{ start, end }] = ranges
|
|
545
|
+
const length = (end - start + 1)
|
|
770
546
|
|
|
771
|
-
|
|
772
|
-
|
|
547
|
+
return {
|
|
548
|
+
status: 200,
|
|
549
|
+
body: drive.createReadStream(pathname, {
|
|
550
|
+
start,
|
|
551
|
+
end
|
|
552
|
+
}),
|
|
553
|
+
headers: {
|
|
554
|
+
...resHeaders,
|
|
555
|
+
'Content-Length': `${length}`,
|
|
556
|
+
'Content-Range': `bytes ${start}-${end}/${size}`
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
status: 200,
|
|
563
|
+
headers: {
|
|
564
|
+
...resHeaders,
|
|
565
|
+
'Content-Length': `${size}`
|
|
566
|
+
},
|
|
567
|
+
body: drive.createReadStream(pathname)
|
|
773
568
|
}
|
|
569
|
+
}
|
|
774
570
|
|
|
775
|
-
|
|
571
|
+
async function listEntries (drive, pathname = '/') {
|
|
572
|
+
const entries = []
|
|
573
|
+
for await (const path of drive.readdir(pathname)) {
|
|
574
|
+
const stat = await drive.entry(path)
|
|
575
|
+
if (stat === null) {
|
|
576
|
+
entries.push(path + '/')
|
|
577
|
+
} else {
|
|
578
|
+
entries.push(path)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return entries
|
|
776
582
|
}
|
|
777
583
|
|
|
778
584
|
function formatPeers (peers) {
|
|
779
|
-
return peers.map((
|
|
585
|
+
return peers.map((peer) => {
|
|
586
|
+
const remotePublicKey = peer.remotePublicKey.toString('hex')
|
|
587
|
+
const remoteHost = peer.stream?.rawStream?.remoteHost
|
|
780
588
|
return {
|
|
781
|
-
remotePublicKey
|
|
782
|
-
|
|
783
|
-
remoteAddress,
|
|
784
|
-
stats
|
|
589
|
+
remotePublicKey,
|
|
590
|
+
remoteHost
|
|
785
591
|
}
|
|
786
592
|
})
|
|
787
593
|
}
|
|
594
|
+
|
|
595
|
+
function getMimeType (path) {
|
|
596
|
+
let mimeType = mime.getType(path) || 'text/plain; charset=utf-8'
|
|
597
|
+
if (mimeType.startsWith('text/')) mimeType = `${mimeType}; charset=utf-8`
|
|
598
|
+
return mimeType
|
|
599
|
+
}
|