most-box 0.0.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/build.mjs +40 -0
- package/cli.js +2 -0
- package/package.json +44 -0
- package/public/app.jsx +1335 -0
- package/public/bundle.js +111 -0
- package/public/bundle.js.map +7 -0
- package/public/favicon.ico +0 -0
- package/public/icons/apple-touch-icon.png +0 -0
- package/public/icons/mask-icon.svg +3 -0
- package/public/icons/most.png +0 -0
- package/public/icons/pwa-192x192.png +0 -0
- package/public/icons/pwa-512x512.png +0 -0
- package/public/index.html +15 -0
- package/public/index.jsx +5 -0
- package/server.js +615 -0
- package/src/config.js +22 -0
- package/src/core/cid.js +141 -0
- package/src/index.js +1073 -0
- package/src/utils/errors.js +66 -0
- package/src/utils/security.js +166 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="52" height="49" viewBox="0 0 52 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M12.6085 35.2507C20.0705 44.0321 28.8029 45.8682 39.3058 39.9408L46.3677 47.7741L51.0789 43.0741L44.017 36.0291C47.758 31.0397 49.8006 22.6726 47.5 14.5C45.1994 6.32739 38.8556 1.32293 31.4536 0.784076C44.017 10.1741 44.017 23.2741 37.7353 30.5507L18.07 10.1741L25.9221 3.5482L14.1789 0.774098L1.61554 13.2975L8.67745 20.3425L14.1689 14.8641L33.0741 34.4524C31.7048 35.576 30.0916 36.3653 28.3628 36.7575C21.3609 38.2943 12.9686 30.5308 12.6585 30.5308C12.3484 30.5308 2.45577 39.9308 2.45577 39.9308C0.805318 41.238 0.785313 42.3057 1.62554 43.2338L4.76639 45.4291C5.99673 45.7285 7.05701 45.2894 7.90724 43.8624L12.6085 35.2507Z" fill="black" stroke="black"/>
|
|
3
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN" data-theme="light">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>MostBox 文件管理</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="./bundle.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
|
|
15
|
+
</html>
|
package/public/index.jsx
ADDED
package/server.js
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
import { exec } from 'node:child_process'
|
|
8
|
+
import { MostBoxEngine } from './src/index.js'
|
|
9
|
+
import { parseMostLink } from './src/core/cid.js'
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const PORT = Number(process.env.MOSTBOX_PORT) || 1976
|
|
13
|
+
const HOST = '127.0.0.1'
|
|
14
|
+
|
|
15
|
+
const wsClients = new Set()
|
|
16
|
+
let engine = null
|
|
17
|
+
let serverInstance = null
|
|
18
|
+
|
|
19
|
+
// --- Config ---
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), '.most-box')
|
|
21
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
22
|
+
|
|
23
|
+
function loadConfig() {
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
26
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'))
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[Config] Load error:', err.message)
|
|
30
|
+
}
|
|
31
|
+
return {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveConfig(config) {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
37
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
38
|
+
}
|
|
39
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8')
|
|
40
|
+
return true
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error('[Config] Save error:', err.message)
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Storage path ---
|
|
48
|
+
function getDataPath() {
|
|
49
|
+
const config = loadConfig()
|
|
50
|
+
return config.dataPath || path.join(os.homedir(), 'most-data')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Static file serving ---
|
|
54
|
+
const MIME_TYPES = {
|
|
55
|
+
'.html': 'text/html; charset=utf-8',
|
|
56
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
57
|
+
'.css': 'text/css; charset=utf-8',
|
|
58
|
+
'.json': 'application/json',
|
|
59
|
+
'.png': 'image/png',
|
|
60
|
+
'.jpg': 'image/jpeg',
|
|
61
|
+
'.jpeg': 'image/jpeg',
|
|
62
|
+
'.gif': 'image/gif',
|
|
63
|
+
'.webp': 'image/webp',
|
|
64
|
+
'.svg': 'image/svg+xml',
|
|
65
|
+
'.ico': 'image/x-icon',
|
|
66
|
+
'.mp4': 'video/mp4',
|
|
67
|
+
'.webm': 'video/webm',
|
|
68
|
+
'.ogg': 'video/ogg',
|
|
69
|
+
'.mp3': 'audio/mpeg',
|
|
70
|
+
'.wav': 'audio/wav',
|
|
71
|
+
'.flac': 'audio/flac',
|
|
72
|
+
'.aac': 'audio/aac',
|
|
73
|
+
'.m4a': 'audio/mp4',
|
|
74
|
+
'.opus': 'audio/opus',
|
|
75
|
+
'.woff2': 'font/woff2',
|
|
76
|
+
'.woff': 'font/woff'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function serveStatic(req, res) {
|
|
80
|
+
let filePath = req.url === '/' ? '/index.html' : req.url
|
|
81
|
+
filePath = filePath.split('?')[0]
|
|
82
|
+
|
|
83
|
+
const fullPath = path.join(__dirname, 'public', filePath)
|
|
84
|
+
const ext = path.extname(fullPath)
|
|
85
|
+
const publicDir = path.join(__dirname, 'public')
|
|
86
|
+
|
|
87
|
+
if (!fullPath.startsWith(publicDir)) {
|
|
88
|
+
res.writeHead(403)
|
|
89
|
+
res.end('Forbidden')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fs.readFile(fullPath, (err, data) => {
|
|
94
|
+
if (err) {
|
|
95
|
+
res.writeHead(404)
|
|
96
|
+
res.end('Not found')
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let content = data
|
|
101
|
+
if (ext === '.html') {
|
|
102
|
+
content = data.toString()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' })
|
|
106
|
+
res.end(content)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Streaming multipart parser for large files ---
|
|
111
|
+
async function parseMultipart(req) {
|
|
112
|
+
const boundaryMatch = req.headers['content-type']?.match(/boundary=(?:"([^"]+)"|([^\s;]+))/)
|
|
113
|
+
if (!boundaryMatch) throw new Error('No boundary in content-type')
|
|
114
|
+
const boundary = boundaryMatch[1] || boundaryMatch[2]
|
|
115
|
+
|
|
116
|
+
const chunks = []
|
|
117
|
+
for await (const chunk of req) {
|
|
118
|
+
chunks.push(chunk)
|
|
119
|
+
}
|
|
120
|
+
const buffer = Buffer.concat(chunks)
|
|
121
|
+
|
|
122
|
+
const parts = []
|
|
123
|
+
const boundaryBuf = Buffer.from('--' + boundary)
|
|
124
|
+
let start = 0
|
|
125
|
+
|
|
126
|
+
while (true) {
|
|
127
|
+
const idx = buffer.indexOf(boundaryBuf, start)
|
|
128
|
+
if (idx === -1) break
|
|
129
|
+
|
|
130
|
+
if (start > 0) {
|
|
131
|
+
// Handle both \r\n and \n line endings
|
|
132
|
+
let partStart = start
|
|
133
|
+
if (buffer[partStart] === 0x0d && buffer[partStart + 1] === 0x0a) {
|
|
134
|
+
partStart += 2
|
|
135
|
+
} else if (buffer[partStart] === 0x0a) {
|
|
136
|
+
partStart += 1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let partEnd = idx - 1
|
|
140
|
+
if (buffer[partEnd] === 0x0a) {
|
|
141
|
+
partEnd--
|
|
142
|
+
if (buffer[partEnd] === 0x0d) {
|
|
143
|
+
partEnd--
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const partData = buffer.slice(partStart, partEnd + 1)
|
|
148
|
+
|
|
149
|
+
const headerEnd = partData.indexOf('\r\n\r\n')
|
|
150
|
+
const headerEndAlt = partData.indexOf('\n\n')
|
|
151
|
+
|
|
152
|
+
let headerEndIdx = -1
|
|
153
|
+
let bodyStart = -1
|
|
154
|
+
|
|
155
|
+
if (headerEnd !== -1) {
|
|
156
|
+
headerEndIdx = headerEnd
|
|
157
|
+
bodyStart = headerEnd + 4
|
|
158
|
+
} else if (headerEndAlt !== -1) {
|
|
159
|
+
headerEndIdx = headerEndAlt
|
|
160
|
+
bodyStart = headerEndAlt + 2
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (headerEndIdx !== -1) {
|
|
164
|
+
const headers = partData.slice(0, headerEndIdx).toString()
|
|
165
|
+
const body = partData.slice(bodyStart)
|
|
166
|
+
|
|
167
|
+
const nameMatch = headers.match(/name="([^"]+)"/)
|
|
168
|
+
const filenameMatch = headers.match(/filename="([^"]+)"/)
|
|
169
|
+
parts.push({
|
|
170
|
+
name: nameMatch?.[1],
|
|
171
|
+
filename: filenameMatch?.[1],
|
|
172
|
+
data: body,
|
|
173
|
+
headers
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Move to after the boundary
|
|
179
|
+
start = idx + boundaryBuf.length
|
|
180
|
+
// Skip optional whitespace after boundary
|
|
181
|
+
while (start < buffer.length && (buffer[start] === 0x20 || buffer[start] === 0x09)) {
|
|
182
|
+
start++
|
|
183
|
+
}
|
|
184
|
+
// Skip line ending
|
|
185
|
+
if (start < buffer.length && buffer[start] === 0x0d) {
|
|
186
|
+
start++
|
|
187
|
+
}
|
|
188
|
+
if (start < buffer.length && buffer[start] === 0x0a) {
|
|
189
|
+
start++
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return parts
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- JSON body parser ---
|
|
197
|
+
async function parseJSON(req) {
|
|
198
|
+
const chunks = []
|
|
199
|
+
for await (const chunk of req) {
|
|
200
|
+
chunks.push(chunk)
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(Buffer.concat(chunks).toString())
|
|
204
|
+
} catch {
|
|
205
|
+
return {}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- API Routes ---
|
|
210
|
+
async function handleAPI(req, res) {
|
|
211
|
+
const url = new URL(req.url, `http://${HOST}:${PORT}`)
|
|
212
|
+
const pathname = url.pathname
|
|
213
|
+
const method = req.method
|
|
214
|
+
|
|
215
|
+
const json = (data, status = 200) => {
|
|
216
|
+
res.writeHead(status, { 'Content-Type': 'application/json' })
|
|
217
|
+
res.end(JSON.stringify(data))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// GET /api/node-id
|
|
222
|
+
if (pathname === '/api/node-id' && method === 'GET') {
|
|
223
|
+
json({ id: engine.getNodeId() })
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// GET /api/config
|
|
228
|
+
if (pathname === '/api/config' && method === 'GET') {
|
|
229
|
+
const config = loadConfig()
|
|
230
|
+
json({ dataPath: config.dataPath || '' })
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// POST /api/config
|
|
235
|
+
if (pathname === '/api/config' && method === 'POST') {
|
|
236
|
+
const body = await parseJSON(req)
|
|
237
|
+
const config = loadConfig()
|
|
238
|
+
|
|
239
|
+
if (body.resetStorage) {
|
|
240
|
+
config.dataPath = ''
|
|
241
|
+
} else if (body.dataPath !== undefined) {
|
|
242
|
+
let dataPath = body.dataPath.trim()
|
|
243
|
+
let basePath = dataPath
|
|
244
|
+
|
|
245
|
+
if (dataPath.match(/^[A-Za-z]:\\$/)) {
|
|
246
|
+
basePath = dataPath
|
|
247
|
+
dataPath = path.join(dataPath, 'most-data')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!fs.existsSync(basePath)) {
|
|
251
|
+
json({ error: '目录不存在' }, 400)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!fs.existsSync(dataPath)) {
|
|
256
|
+
fs.mkdirSync(dataPath, { recursive: true })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
config.dataPath = dataPath
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const success = saveConfig(config)
|
|
263
|
+
json({ success, dataPath: getDataPath() })
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// GET /api/config/data-path
|
|
268
|
+
if (pathname === '/api/config/data-path' && method === 'GET') {
|
|
269
|
+
const config = loadConfig()
|
|
270
|
+
const isDefault = !config.dataPath
|
|
271
|
+
const dataPath = getDataPath()
|
|
272
|
+
json({ dataPath, isDefault })
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// GET /api/network-status
|
|
277
|
+
if (pathname === '/api/network-status' && method === 'GET') {
|
|
278
|
+
json(engine.getNetworkStatus())
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// GET /api/files
|
|
283
|
+
if (pathname === '/api/files' && method === 'GET') {
|
|
284
|
+
json(engine.listPublishedFiles())
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// POST /api/publish — multipart file upload
|
|
289
|
+
if (pathname === '/api/publish' && method === 'POST') {
|
|
290
|
+
const parts = await parseMultipart(req)
|
|
291
|
+
|
|
292
|
+
const filePart = parts.find(p => p.name === 'file')
|
|
293
|
+
if (!filePart || !filePart.filename) {
|
|
294
|
+
json({ error: 'No file provided' }, 400)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const result = await engine.publishFile(filePart.data, filePart.filename)
|
|
299
|
+
|
|
300
|
+
json({ success: true, ...result })
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// POST /api/download — start async download from P2P
|
|
305
|
+
if (pathname === '/api/download' && method === 'POST') {
|
|
306
|
+
const body = await parseJSON(req)
|
|
307
|
+
if (!body.link) {
|
|
308
|
+
json({ error: 'link is required' }, 400)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
313
|
+
|
|
314
|
+
// Parse link to check if file already exists
|
|
315
|
+
const parsed = parseMostLink(body.link)
|
|
316
|
+
if (parsed.error) {
|
|
317
|
+
json({ error: parsed.error }, 400)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check if file already exists in published files
|
|
322
|
+
const existingFile = engine.getPublishedFiles().find(f => f.cid === parsed.cid)
|
|
323
|
+
if (existingFile) {
|
|
324
|
+
console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
|
|
325
|
+
json({ success: true, taskId, alreadyExists: true, fileName: existingFile.fileName })
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Async download — do not block HTTP response
|
|
330
|
+
engine.downloadFile(body.link, taskId).catch(err => {
|
|
331
|
+
if (err.message === 'Download cancelled') {
|
|
332
|
+
wsBroadcast('download:cancelled', { taskId })
|
|
333
|
+
} else {
|
|
334
|
+
wsBroadcast('download:error', { taskId, error: err.message })
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
json({ success: true, taskId })
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// POST /api/download/cancel — cancel an active download
|
|
343
|
+
if (pathname === '/api/download/cancel' && method === 'POST') {
|
|
344
|
+
const body = await parseJSON(req)
|
|
345
|
+
if (!body.taskId) {
|
|
346
|
+
json({ error: 'taskId is required' }, 400)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
engine.cancelDownload(body.taskId)
|
|
350
|
+
json({ success: true })
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// DELETE /api/files/:cid
|
|
355
|
+
if (pathname.startsWith('/api/files/') && method === 'DELETE') {
|
|
356
|
+
const cid = pathname.replace('/api/files/', '').replace(/\/$/, '')
|
|
357
|
+
const result = await engine.deletePublishedFile(cid)
|
|
358
|
+
json(result)
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// POST /api/move — rename/move a published file (changes path without re-uploading)
|
|
363
|
+
if (pathname === '/api/move' && method === 'POST') {
|
|
364
|
+
const body = await parseJSON(req)
|
|
365
|
+
if (!body.cid || !body.newFileName) {
|
|
366
|
+
json({ error: 'cid and newFileName are required' }, 400)
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const result = await engine.moveFile(body.cid, body.newFileName)
|
|
371
|
+
json({ success: true, ...result })
|
|
372
|
+
} catch (err) {
|
|
373
|
+
json({ error: err.message }, 400)
|
|
374
|
+
}
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// POST /api/folder/rename — rename a folder (renames all files within)
|
|
379
|
+
if (pathname === '/api/folder/rename' && method === 'POST') {
|
|
380
|
+
const body = await parseJSON(req)
|
|
381
|
+
if (!body.oldPath || !body.newPath) {
|
|
382
|
+
json({ error: 'oldPath and newPath are required' }, 400)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const result = engine.renameFolder(body.oldPath, body.newPath)
|
|
387
|
+
json({ success: true, ...result })
|
|
388
|
+
} catch (err) {
|
|
389
|
+
json({ error: err.message }, 400)
|
|
390
|
+
}
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// GET /api/files/:cid/download — serve file inline for preview / download
|
|
395
|
+
if (pathname.match(/^\/api\/files\/[^/]+\/download$/) && method === 'GET') {
|
|
396
|
+
json({ error: 'Use P2P network to download this file' }, 400)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// POST /api/shutdown — graceful server shutdown
|
|
401
|
+
if (pathname === '/api/shutdown' && method === 'POST') {
|
|
402
|
+
json({ success: true })
|
|
403
|
+
console.log('[MostBox] Shutdown requested via API...')
|
|
404
|
+
setTimeout(async () => {
|
|
405
|
+
await engine.stop()
|
|
406
|
+
serverInstance.close()
|
|
407
|
+
console.log('[MostBox] Server stopped.')
|
|
408
|
+
process.exit(0)
|
|
409
|
+
}, 100)
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// GET /api/trash — list trash files
|
|
414
|
+
if (pathname === '/api/trash' && method === 'GET') {
|
|
415
|
+
json(engine.listTrashFiles())
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// POST /api/trash/:cid/restore — restore file from trash
|
|
420
|
+
if (pathname.match(/^\/api\/trash\/[^/]+\/restore$/) && method === 'POST') {
|
|
421
|
+
const cid = pathname.split('/')[3]
|
|
422
|
+
try {
|
|
423
|
+
const result = engine.restoreTrashFile(cid)
|
|
424
|
+
json({ success: true, files: result })
|
|
425
|
+
} catch (err) {
|
|
426
|
+
json({ error: err.message }, 400)
|
|
427
|
+
}
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// DELETE /api/trash/:cid — permanently delete a trash file
|
|
432
|
+
if (pathname.match(/^\/api\/trash\/[^/]+$/) && method === 'DELETE') {
|
|
433
|
+
const cid = pathname.split('/')[3]
|
|
434
|
+
const result = await engine.permanentDeleteTrashFile(cid)
|
|
435
|
+
json({ success: true, trashFiles: result })
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// DELETE /api/trash — empty trash
|
|
440
|
+
if (pathname === '/api/trash' && method === 'DELETE') {
|
|
441
|
+
const result = await engine.emptyTrash()
|
|
442
|
+
json({ success: true, trashFiles: result })
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// POST /api/files/:cid/star — toggle starred status
|
|
447
|
+
if (pathname.match(/^\/api\/files\/[^/]+\/star$/) && method === 'POST') {
|
|
448
|
+
const cid = pathname.split('/')[3]
|
|
449
|
+
try {
|
|
450
|
+
const result = engine.toggleStarred(cid)
|
|
451
|
+
json({ success: true, ...result })
|
|
452
|
+
} catch (err) {
|
|
453
|
+
json({ error: err.message }, 400)
|
|
454
|
+
}
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// GET /api/storage — get storage statistics
|
|
459
|
+
if (pathname === '/api/storage' && method === 'GET') {
|
|
460
|
+
const result = await engine.getStorageStats()
|
|
461
|
+
json(result)
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
json({ error: 'Not found' }, 404)
|
|
466
|
+
} catch (err) {
|
|
467
|
+
console.error('[API Error]', err)
|
|
468
|
+
json({ error: err.message, code: err.code }, 500)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// --- Minimal WebSocket (RFC 6455) ---
|
|
473
|
+
function upgradeToWebSocket(req, socket) {
|
|
474
|
+
const key = req.headers['sec-websocket-key']
|
|
475
|
+
if (!key) { socket.destroy(); return }
|
|
476
|
+
|
|
477
|
+
const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
478
|
+
const accept = crypto.createHash('sha1')
|
|
479
|
+
.update(key + MAGIC)
|
|
480
|
+
.digest('base64')
|
|
481
|
+
|
|
482
|
+
socket.write(
|
|
483
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
484
|
+
'Upgrade: websocket\r\n' +
|
|
485
|
+
'Connection: Upgrade\r\n' +
|
|
486
|
+
`Sec-WebSocket-Accept: ${accept}\r\n` +
|
|
487
|
+
'\r\n'
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
wsClients.add(socket)
|
|
491
|
+
socket.on('close', () => wsClients.delete(socket))
|
|
492
|
+
socket.on('error', () => wsClients.delete(socket))
|
|
493
|
+
|
|
494
|
+
socket.on('data', (buf) => {
|
|
495
|
+
if (buf.length < 2) return
|
|
496
|
+
const opcode = buf[0] & 0x0f
|
|
497
|
+
if (opcode === 0x8) {
|
|
498
|
+
wsClients.delete(socket)
|
|
499
|
+
socket.end()
|
|
500
|
+
}
|
|
501
|
+
if (opcode === 0x9) {
|
|
502
|
+
const pong = Buffer.from(buf)
|
|
503
|
+
pong[0] = (pong[0] & 0xf0) | 0xa
|
|
504
|
+
socket.write(pong)
|
|
505
|
+
}
|
|
506
|
+
if (opcode === 0x1 || opcode === 0x2) {
|
|
507
|
+
// Text or binary message - could broadcast to other clients if needed
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function wsBroadcast(event, data) {
|
|
513
|
+
const payload = JSON.stringify({ event, data })
|
|
514
|
+
const buf = Buffer.from(payload)
|
|
515
|
+
|
|
516
|
+
let frame
|
|
517
|
+
if (buf.length < 126) {
|
|
518
|
+
frame = Buffer.alloc(2 + buf.length)
|
|
519
|
+
frame[0] = 0x81
|
|
520
|
+
frame[1] = buf.length
|
|
521
|
+
buf.copy(frame, 2)
|
|
522
|
+
} else if (buf.length < 65536) {
|
|
523
|
+
frame = Buffer.alloc(4 + buf.length)
|
|
524
|
+
frame[0] = 0x81
|
|
525
|
+
frame[1] = 126
|
|
526
|
+
frame.writeUInt16BE(buf.length, 2)
|
|
527
|
+
buf.copy(frame, 4)
|
|
528
|
+
} else {
|
|
529
|
+
frame = Buffer.alloc(10 + buf.length)
|
|
530
|
+
frame[0] = 0x81
|
|
531
|
+
frame[1] = 127
|
|
532
|
+
frame.writeBigUInt64BE(BigInt(buf.length), 2)
|
|
533
|
+
buf.copy(frame, 10)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
for (const client of wsClients) {
|
|
537
|
+
try { client.write(frame) } catch {}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// --- Main ---
|
|
542
|
+
async function main() {
|
|
543
|
+
console.log('[MostBox] Starting core daemon...')
|
|
544
|
+
|
|
545
|
+
const dataPath = getDataPath()
|
|
546
|
+
console.log(`[MostBox] Storage: ${dataPath}`)
|
|
547
|
+
|
|
548
|
+
engine = new MostBoxEngine({ dataPath })
|
|
549
|
+
|
|
550
|
+
engine.on('download:progress', (data) => wsBroadcast('download:progress', data))
|
|
551
|
+
engine.on('download:status', (data) => wsBroadcast('download:status', data))
|
|
552
|
+
engine.on('download:success', (data) => wsBroadcast('download:success', data))
|
|
553
|
+
engine.on('download:cancelled', (data) => wsBroadcast('download:cancelled', data))
|
|
554
|
+
engine.on('publish:progress', (data) => wsBroadcast('publish:progress', data))
|
|
555
|
+
engine.on('publish:success', (data) => wsBroadcast('publish:success', data))
|
|
556
|
+
engine.on('connection', () => {
|
|
557
|
+
wsBroadcast('network:status', engine.getNetworkStatus())
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
await engine.start()
|
|
561
|
+
console.log('[MostBox] Engine ready')
|
|
562
|
+
|
|
563
|
+
serverInstance = http.createServer((req, res) => {
|
|
564
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
565
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
566
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
|
567
|
+
|
|
568
|
+
if (req.method === 'OPTIONS') {
|
|
569
|
+
res.writeHead(204)
|
|
570
|
+
res.end()
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (req.url.startsWith('/api/')) {
|
|
575
|
+
handleAPI(req, res)
|
|
576
|
+
} else {
|
|
577
|
+
serveStatic(req, res)
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
serverInstance.on('upgrade', (req, socket) => {
|
|
582
|
+
if (req.url.startsWith('/ws')) {
|
|
583
|
+
upgradeToWebSocket(req, socket)
|
|
584
|
+
} else {
|
|
585
|
+
socket.destroy()
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
serverInstance.listen(PORT, HOST, () => {
|
|
590
|
+
const url = `http://${HOST}:${PORT}`
|
|
591
|
+
console.log(`[MostBox] Server running at ${url}`)
|
|
592
|
+
|
|
593
|
+
const cmd = process.platform === 'win32' ? 'start ""'
|
|
594
|
+
: process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
595
|
+
exec(`${cmd} "${url}"`)
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
process.on('SIGINT', async () => {
|
|
599
|
+
console.log('\n[MostBox] Shutting down...')
|
|
600
|
+
await engine.stop()
|
|
601
|
+
serverInstance.close()
|
|
602
|
+
process.exit(0)
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
process.on('SIGTERM', async () => {
|
|
606
|
+
await engine.stop()
|
|
607
|
+
serverInstance.close()
|
|
608
|
+
process.exit(0)
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
main().catch(err => {
|
|
613
|
+
console.error('[MostBox] Fatal error:', err)
|
|
614
|
+
process.exit(1)
|
|
615
|
+
})
|
package/src/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// File size limits
|
|
6
|
+
export const MAX_FILE_SIZE = 100 * 1024 * 1024 * 1024 // 100 GB
|
|
7
|
+
|
|
8
|
+
// Network timeouts (ms)
|
|
9
|
+
export const CONNECTION_TIMEOUT = 120000
|
|
10
|
+
export const DOWNLOAD_TIMEOUT = 900000
|
|
11
|
+
|
|
12
|
+
// P2P settings
|
|
13
|
+
export const GLOBAL_SHARED_SEED_STRING = 'most-box-global-shared-seed-v1'
|
|
14
|
+
|
|
15
|
+
// DHT Bootstrap nodes for Hyperswarm/HyperDHT
|
|
16
|
+
// Using the same bootstrap nodes as Keet.io/HyperDHT for compatibility
|
|
17
|
+
// Format: [suggested-IP@]<host>:<port> to avoid DNS calls
|
|
18
|
+
export const SWARM_BOOTSTRAP = [
|
|
19
|
+
'88.99.3.86@node1.hyperdht.org:49737',
|
|
20
|
+
'142.93.90.113@node2.hyperdht.org:49737',
|
|
21
|
+
'138.68.147.8@node3.hyperdht.org:49737'
|
|
22
|
+
]
|