most-box 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -73
- package/cli.js +2 -2
- package/package.json +9 -5
- package/public/app.css +1519 -0
- package/public/app.jsx +607 -399
- package/public/bundle.css +1 -0
- package/public/bundle.js +10 -14
- package/public/error-boundary.jsx +50 -0
- package/public/index.html +2 -1
- package/public/index.jsx +16 -1
- package/server.js +280 -197
- package/src/config.js +24 -7
- package/src/core/cid.js +23 -18
- package/src/index.js +400 -272
- package/src/utils/security.js +27 -24
- package/public/bundle.js.map +0 -7
- package/public/icons/apple-touch-icon.png +0 -0
- package/public/icons/mask-icon.svg +0 -3
- package/public/icons/most.png +0 -0
- package/public/icons/pwa-192x192.png +0 -0
- package/public/icons/pwa-512x512.png +0 -0
package/server.js
CHANGED
|
@@ -3,20 +3,30 @@ import fs from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import os from 'node:os'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import Busboy from 'busboy'
|
|
8
|
+
import { WebSocketServer } from 'ws'
|
|
8
9
|
import { MostBoxEngine } from './src/index.js'
|
|
9
10
|
import { parseMostLink } from './src/core/cid.js'
|
|
11
|
+
import { MAX_FILE_SIZE } from './src/config.js'
|
|
10
12
|
|
|
11
13
|
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
|
+
const PORT = Number(process.env.MOSTBOX_PORT || process.env.PORT) || 1976
|
|
15
|
+
const HOST = process.env.MOSTBOX_HOST || '127.0.0.1'
|
|
16
|
+
|
|
17
|
+
const MAX_JSON_BODY_SIZE = 10 * 1024 * 1024 // 10MB
|
|
18
|
+
const MAX_UPLOAD_SIZE = MAX_FILE_SIZE
|
|
19
|
+
const UPLOAD_TMP_DIR = path.join(os.tmpdir(), 'most-box-uploads')
|
|
20
|
+
|
|
21
|
+
const rateLimitMap = new Map()
|
|
22
|
+
const RATE_LIMIT_WINDOW = 60 * 1000
|
|
23
|
+
const RATE_LIMIT_MAX_REQUESTS = 120
|
|
14
24
|
|
|
15
|
-
const wsClients = new Set()
|
|
16
25
|
let engine = null
|
|
17
26
|
let serverInstance = null
|
|
27
|
+
let wss = null
|
|
18
28
|
|
|
19
|
-
// ---
|
|
29
|
+
// --- 配置 ---
|
|
20
30
|
const CONFIG_DIR = path.join(os.homedir(), '.most-box')
|
|
21
31
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
22
32
|
|
|
@@ -36,7 +46,9 @@ function saveConfig(config) {
|
|
|
36
46
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
37
47
|
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
38
48
|
}
|
|
39
|
-
|
|
49
|
+
const tmpPath = CONFIG_FILE + '.tmp'
|
|
50
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
|
|
51
|
+
fs.renameSync(tmpPath, CONFIG_FILE)
|
|
40
52
|
return true
|
|
41
53
|
} catch (err) {
|
|
42
54
|
console.error('[Config] Save error:', err.message)
|
|
@@ -44,13 +56,33 @@ function saveConfig(config) {
|
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
// ---
|
|
59
|
+
// --- 存储路径 ---
|
|
48
60
|
function getDataPath() {
|
|
49
61
|
const config = loadConfig()
|
|
50
62
|
return config.dataPath || path.join(os.homedir(), 'most-data')
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
// ---
|
|
65
|
+
// --- 速率限制 ---
|
|
66
|
+
function checkRateLimit(clientIp) {
|
|
67
|
+
const now = Date.now()
|
|
68
|
+
if (!rateLimitMap.has(clientIp)) {
|
|
69
|
+
rateLimitMap.set(clientIp, [])
|
|
70
|
+
}
|
|
71
|
+
const requests = rateLimitMap.get(clientIp)
|
|
72
|
+
while (requests.length > 0 && requests[0] < now - RATE_LIMIT_WINDOW) {
|
|
73
|
+
requests.shift()
|
|
74
|
+
}
|
|
75
|
+
if (requests.length === 0) {
|
|
76
|
+
rateLimitMap.delete(clientIp)
|
|
77
|
+
}
|
|
78
|
+
if (requests.length >= RATE_LIMIT_MAX_REQUESTS) {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
requests.push(now)
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- 静态文件服务 ---
|
|
54
86
|
const MIME_TYPES = {
|
|
55
87
|
'.html': 'text/html; charset=utf-8',
|
|
56
88
|
'.js': 'application/javascript; charset=utf-8',
|
|
@@ -76,6 +108,11 @@ const MIME_TYPES = {
|
|
|
76
108
|
'.woff': 'font/woff'
|
|
77
109
|
}
|
|
78
110
|
|
|
111
|
+
function getMimeType(fileName) {
|
|
112
|
+
const ext = path.extname(fileName).toLowerCase()
|
|
113
|
+
return MIME_TYPES[ext] || 'application/octet-stream'
|
|
114
|
+
}
|
|
115
|
+
|
|
79
116
|
function serveStatic(req, res) {
|
|
80
117
|
let filePath = req.url === '/' ? '/index.html' : req.url
|
|
81
118
|
filePath = filePath.split('?')[0]
|
|
@@ -97,123 +134,139 @@ function serveStatic(req, res) {
|
|
|
97
134
|
return
|
|
98
135
|
}
|
|
99
136
|
|
|
100
|
-
let content = data
|
|
101
|
-
if (ext === '.html') {
|
|
102
|
-
content = data.toString()
|
|
103
|
-
}
|
|
104
|
-
|
|
105
137
|
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' })
|
|
106
|
-
res.end(
|
|
138
|
+
res.end(data)
|
|
107
139
|
})
|
|
108
140
|
}
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
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]
|
|
142
|
+
function decodeFilenameFromHeader(headerStr) {
|
|
143
|
+
if (!headerStr) return null
|
|
115
144
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
145
|
+
const filenameStarMatch = headerStr.match(/filename\*=(?:UTF-8''|utf-8'')([^;\r\n]+)/i)
|
|
146
|
+
if (filenameStarMatch) {
|
|
147
|
+
return decodeURIComponent(filenameStarMatch[1])
|
|
119
148
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
149
|
+
|
|
150
|
+
const filenameMatch = headerStr.match(/filename="([^"]+)"/)
|
|
151
|
+
if (filenameMatch) {
|
|
152
|
+
const rawFilename = filenameMatch[1]
|
|
153
|
+
try {
|
|
154
|
+
const buf = Buffer.from(rawFilename, 'latin1')
|
|
155
|
+
const decoded = buf.toString('utf8')
|
|
156
|
+
if (decoded.includes('\ufffd')) {
|
|
157
|
+
return rawFilename
|
|
137
158
|
}
|
|
159
|
+
return decoded
|
|
160
|
+
} catch {
|
|
161
|
+
return rawFilename
|
|
162
|
+
}
|
|
163
|
+
}
|
|
138
164
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
const filenamePlainMatch = headerStr.match(/filename=([^;\r\n]+)/)
|
|
166
|
+
if (filenamePlainMatch) {
|
|
167
|
+
return filenamePlainMatch[1].trim()
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseMultipartBusboy(req) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
if (!fs.existsSync(UPLOAD_TMP_DIR)) {
|
|
175
|
+
fs.mkdirSync(UPLOAD_TMP_DIR, { recursive: true })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const busboy = Busboy({
|
|
179
|
+
headers: req.headers,
|
|
180
|
+
limits: {
|
|
181
|
+
fileSize: MAX_UPLOAD_SIZE,
|
|
182
|
+
files: 1,
|
|
183
|
+
fields: 0
|
|
145
184
|
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const result = { filePath: null, filename: null }
|
|
188
|
+
let fileSize = 0
|
|
189
|
+
let writeStream = null
|
|
190
|
+
let tempPath = null
|
|
191
|
+
|
|
192
|
+
busboy.on('file', (name, stream, info) => {
|
|
193
|
+
result.filename = decodeFilenameFromHeader(`filename="${info.filename}"`)
|
|
194
|
+
tempPath = path.join(UPLOAD_TMP_DIR, `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`)
|
|
195
|
+
writeStream = fs.createWriteStream(tempPath)
|
|
196
|
+
|
|
197
|
+
stream.on('data', (chunk) => {
|
|
198
|
+
fileSize += chunk.length
|
|
199
|
+
if (fileSize > MAX_UPLOAD_SIZE) {
|
|
200
|
+
stream.destroy()
|
|
201
|
+
writeStream.destroy()
|
|
202
|
+
fs.unlink(tempPath, () => {})
|
|
203
|
+
reject(new Error('File too large'))
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
})
|
|
146
207
|
|
|
147
|
-
|
|
208
|
+
stream.on('error', () => {
|
|
209
|
+
if (tempPath) fs.unlink(tempPath, () => {})
|
|
210
|
+
})
|
|
148
211
|
|
|
149
|
-
|
|
150
|
-
const headerEndAlt = partData.indexOf('\n\n')
|
|
212
|
+
stream.pipe(writeStream)
|
|
151
213
|
|
|
152
|
-
|
|
153
|
-
|
|
214
|
+
writeStream.on('finish', () => {
|
|
215
|
+
result.filePath = tempPath
|
|
216
|
+
resolve(result)
|
|
217
|
+
})
|
|
154
218
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
bodyStart = headerEndAlt + 2
|
|
161
|
-
}
|
|
219
|
+
writeStream.on('error', (err) => {
|
|
220
|
+
if (tempPath) fs.unlink(tempPath, () => {})
|
|
221
|
+
reject(err)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
162
224
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
}
|
|
225
|
+
busboy.on('error', (err) => {
|
|
226
|
+
if (tempPath) fs.unlink(tempPath, () => {})
|
|
227
|
+
reject(err)
|
|
228
|
+
})
|
|
177
229
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
}
|
|
230
|
+
busboy.on('close', () => {
|
|
231
|
+
if (!result.filename) {
|
|
232
|
+
resolve(null)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
192
235
|
|
|
193
|
-
|
|
236
|
+
req.on('error', (err) => {
|
|
237
|
+
if (tempPath) fs.unlink(tempPath, () => {})
|
|
238
|
+
reject(err)
|
|
239
|
+
})
|
|
240
|
+
req.pipe(busboy)
|
|
241
|
+
})
|
|
194
242
|
}
|
|
195
243
|
|
|
196
|
-
// --- JSON
|
|
244
|
+
// --- JSON 请求体解析器(带大小限制) ---
|
|
197
245
|
async function parseJSON(req) {
|
|
198
246
|
const chunks = []
|
|
247
|
+
let totalSize = 0
|
|
199
248
|
for await (const chunk of req) {
|
|
249
|
+
totalSize += chunk.length
|
|
250
|
+
if (totalSize > MAX_JSON_BODY_SIZE) {
|
|
251
|
+
throw new Error('Request body too large')
|
|
252
|
+
}
|
|
200
253
|
chunks.push(chunk)
|
|
201
254
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return {}
|
|
255
|
+
const text = Buffer.concat(chunks).toString()
|
|
256
|
+
if (!text.trim()) {
|
|
257
|
+
throw new Error('Empty request body')
|
|
206
258
|
}
|
|
259
|
+
return JSON.parse(text)
|
|
207
260
|
}
|
|
208
261
|
|
|
209
|
-
// --- API
|
|
262
|
+
// --- API 路由 ---
|
|
210
263
|
async function handleAPI(req, res) {
|
|
211
264
|
const url = new URL(req.url, `http://${HOST}:${PORT}`)
|
|
212
265
|
const pathname = url.pathname
|
|
213
266
|
const method = req.method
|
|
214
267
|
|
|
215
268
|
const json = (data, status = 200) => {
|
|
216
|
-
res.writeHead(status, { 'Content-Type': 'application/json' })
|
|
269
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
|
|
217
270
|
res.end(JSON.stringify(data))
|
|
218
271
|
}
|
|
219
272
|
|
|
@@ -231,34 +284,34 @@ async function handleAPI(req, res) {
|
|
|
231
284
|
return
|
|
232
285
|
}
|
|
233
286
|
|
|
234
|
-
// POST /api/config
|
|
287
|
+
// POST /api/config — 更新配置
|
|
235
288
|
if (pathname === '/api/config' && method === 'POST') {
|
|
236
289
|
const body = await parseJSON(req)
|
|
237
290
|
const config = loadConfig()
|
|
238
|
-
|
|
291
|
+
|
|
239
292
|
if (body.resetStorage) {
|
|
240
293
|
config.dataPath = ''
|
|
241
294
|
} else if (body.dataPath !== undefined) {
|
|
242
295
|
let dataPath = body.dataPath.trim()
|
|
243
296
|
let basePath = dataPath
|
|
244
|
-
|
|
297
|
+
|
|
245
298
|
if (dataPath.match(/^[A-Za-z]:\\$/)) {
|
|
246
299
|
basePath = dataPath
|
|
247
300
|
dataPath = path.join(dataPath, 'most-data')
|
|
248
301
|
}
|
|
249
|
-
|
|
302
|
+
|
|
250
303
|
if (!fs.existsSync(basePath)) {
|
|
251
304
|
json({ error: '目录不存在' }, 400)
|
|
252
305
|
return
|
|
253
306
|
}
|
|
254
|
-
|
|
307
|
+
|
|
255
308
|
if (!fs.existsSync(dataPath)) {
|
|
256
309
|
fs.mkdirSync(dataPath, { recursive: true })
|
|
257
310
|
}
|
|
258
|
-
|
|
311
|
+
|
|
259
312
|
config.dataPath = dataPath
|
|
260
313
|
}
|
|
261
|
-
|
|
314
|
+
|
|
262
315
|
const success = saveConfig(config)
|
|
263
316
|
json({ success, dataPath: getDataPath() })
|
|
264
317
|
return
|
|
@@ -285,23 +338,25 @@ async function handleAPI(req, res) {
|
|
|
285
338
|
return
|
|
286
339
|
}
|
|
287
340
|
|
|
288
|
-
// POST /api/publish — multipart
|
|
341
|
+
// POST /api/publish — multipart 文件上传
|
|
289
342
|
if (pathname === '/api/publish' && method === 'POST') {
|
|
290
|
-
const
|
|
343
|
+
const result = await parseMultipartBusboy(req)
|
|
291
344
|
|
|
292
|
-
|
|
293
|
-
if (!filePart || !filePart.filename) {
|
|
345
|
+
if (!result || !result.filename) {
|
|
294
346
|
json({ error: 'No file provided' }, 400)
|
|
295
347
|
return
|
|
296
348
|
}
|
|
297
349
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
350
|
+
try {
|
|
351
|
+
const publishResult = await engine.publishFile(result.filePath, result.filename)
|
|
352
|
+
json({ success: true, ...publishResult })
|
|
353
|
+
} finally {
|
|
354
|
+
fs.unlink(result.filePath, () => {})
|
|
355
|
+
}
|
|
301
356
|
return
|
|
302
357
|
}
|
|
303
358
|
|
|
304
|
-
// POST /api/download —
|
|
359
|
+
// POST /api/download — 从 P2P 开始异步下载
|
|
305
360
|
if (pathname === '/api/download' && method === 'POST') {
|
|
306
361
|
const body = await parseJSON(req)
|
|
307
362
|
if (!body.link) {
|
|
@@ -311,14 +366,12 @@ async function handleAPI(req, res) {
|
|
|
311
366
|
|
|
312
367
|
const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
313
368
|
|
|
314
|
-
// Parse link to check if file already exists
|
|
315
369
|
const parsed = parseMostLink(body.link)
|
|
316
370
|
if (parsed.error) {
|
|
317
371
|
json({ error: parsed.error }, 400)
|
|
318
372
|
return
|
|
319
373
|
}
|
|
320
374
|
|
|
321
|
-
// Check if file already exists in published files
|
|
322
375
|
const existingFile = engine.getPublishedFiles().find(f => f.cid === parsed.cid)
|
|
323
376
|
if (existingFile) {
|
|
324
377
|
console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
|
|
@@ -326,7 +379,6 @@ async function handleAPI(req, res) {
|
|
|
326
379
|
return
|
|
327
380
|
}
|
|
328
381
|
|
|
329
|
-
// Async download — do not block HTTP response
|
|
330
382
|
engine.downloadFile(body.link, taskId).catch(err => {
|
|
331
383
|
if (err.message === 'Download cancelled') {
|
|
332
384
|
wsBroadcast('download:cancelled', { taskId })
|
|
@@ -339,7 +391,7 @@ async function handleAPI(req, res) {
|
|
|
339
391
|
return
|
|
340
392
|
}
|
|
341
393
|
|
|
342
|
-
// POST /api/download/cancel —
|
|
394
|
+
// POST /api/download/cancel — 取消活动下载
|
|
343
395
|
if (pathname === '/api/download/cancel' && method === 'POST') {
|
|
344
396
|
const body = await parseJSON(req)
|
|
345
397
|
if (!body.taskId) {
|
|
@@ -359,7 +411,7 @@ async function handleAPI(req, res) {
|
|
|
359
411
|
return
|
|
360
412
|
}
|
|
361
413
|
|
|
362
|
-
// POST /api/move —
|
|
414
|
+
// POST /api/move — 重命名/移动已发布文件
|
|
363
415
|
if (pathname === '/api/move' && method === 'POST') {
|
|
364
416
|
const body = await parseJSON(req)
|
|
365
417
|
if (!body.cid || !body.newFileName) {
|
|
@@ -367,7 +419,7 @@ async function handleAPI(req, res) {
|
|
|
367
419
|
return
|
|
368
420
|
}
|
|
369
421
|
try {
|
|
370
|
-
const result =
|
|
422
|
+
const result = engine.moveFile(body.cid, body.newFileName)
|
|
371
423
|
json({ success: true, ...result })
|
|
372
424
|
} catch (err) {
|
|
373
425
|
json({ error: err.message }, 400)
|
|
@@ -375,7 +427,7 @@ async function handleAPI(req, res) {
|
|
|
375
427
|
return
|
|
376
428
|
}
|
|
377
429
|
|
|
378
|
-
// POST /api/folder/rename —
|
|
430
|
+
// POST /api/folder/rename — 重命名文件夹
|
|
379
431
|
if (pathname === '/api/folder/rename' && method === 'POST') {
|
|
380
432
|
const body = await parseJSON(req)
|
|
381
433
|
if (!body.oldPath || !body.newPath) {
|
|
@@ -391,14 +443,62 @@ async function handleAPI(req, res) {
|
|
|
391
443
|
return
|
|
392
444
|
}
|
|
393
445
|
|
|
394
|
-
// GET /api/files/:cid/download —
|
|
446
|
+
// GET /api/files/:cid/download — 内联服务文件,支持 Range
|
|
395
447
|
if (pathname.match(/^\/api\/files\/[^/]+\/download$/) && method === 'GET') {
|
|
396
|
-
|
|
448
|
+
const cid = pathname.split('/')[3]
|
|
449
|
+
const rangeHeader = req.headers['range']
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
if (rangeHeader) {
|
|
453
|
+
const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
|
|
454
|
+
if (rangeMatch) {
|
|
455
|
+
const start = parseInt(rangeMatch[1], 10)
|
|
456
|
+
const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
|
|
457
|
+
|
|
458
|
+
const offset = start
|
|
459
|
+
const limit = end !== undefined ? end - start + 1 : undefined
|
|
460
|
+
|
|
461
|
+
const result = await engine.readFileRaw(cid, { offset, limit })
|
|
462
|
+
const contentType = getMimeType(result.fileName)
|
|
463
|
+
|
|
464
|
+
res.writeHead(206, {
|
|
465
|
+
'Content-Type': contentType,
|
|
466
|
+
'Content-Length': result.buffer.length,
|
|
467
|
+
'Content-Range': `bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`,
|
|
468
|
+
'Accept-Ranges': 'bytes'
|
|
469
|
+
})
|
|
470
|
+
res.end(result.buffer)
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const result = await engine.readFileRaw(cid)
|
|
476
|
+
const contentType = getMimeType(result.fileName)
|
|
477
|
+
res.writeHead(200, {
|
|
478
|
+
'Content-Type': contentType,
|
|
479
|
+
'Content-Length': result.totalSize,
|
|
480
|
+
'Accept-Ranges': 'bytes',
|
|
481
|
+
'Content-Disposition': `inline; filename="${encodeURIComponent(result.fileName)}"`
|
|
482
|
+
})
|
|
483
|
+
res.end(result.buffer)
|
|
484
|
+
} catch (err) {
|
|
485
|
+
if (err.message === 'File not found') {
|
|
486
|
+
json({ error: err.message }, 404)
|
|
487
|
+
} else {
|
|
488
|
+
json({ error: err.message }, 400)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
397
491
|
return
|
|
398
492
|
}
|
|
399
493
|
|
|
400
|
-
// POST /api/shutdown —
|
|
494
|
+
// POST /api/shutdown — 优雅关闭服务器(仅允许 localhost 连接)
|
|
401
495
|
if (pathname === '/api/shutdown' && method === 'POST') {
|
|
496
|
+
const clientIp = req.socket.remoteAddress
|
|
497
|
+
const isLocalhost = clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1'
|
|
498
|
+
if (!isLocalhost) {
|
|
499
|
+
json({ error: 'Forbidden' }, 403)
|
|
500
|
+
return
|
|
501
|
+
}
|
|
402
502
|
json({ success: true })
|
|
403
503
|
console.log('[MostBox] Shutdown requested via API...')
|
|
404
504
|
setTimeout(async () => {
|
|
@@ -410,13 +510,13 @@ async function handleAPI(req, res) {
|
|
|
410
510
|
return
|
|
411
511
|
}
|
|
412
512
|
|
|
413
|
-
// GET /api/trash —
|
|
513
|
+
// GET /api/trash — 列出回收站文件
|
|
414
514
|
if (pathname === '/api/trash' && method === 'GET') {
|
|
415
515
|
json(engine.listTrashFiles())
|
|
416
516
|
return
|
|
417
517
|
}
|
|
418
518
|
|
|
419
|
-
// POST /api/trash/:cid/restore —
|
|
519
|
+
// POST /api/trash/:cid/restore — 从回收站恢复文件
|
|
420
520
|
if (pathname.match(/^\/api\/trash\/[^/]+\/restore$/) && method === 'POST') {
|
|
421
521
|
const cid = pathname.split('/')[3]
|
|
422
522
|
try {
|
|
@@ -428,7 +528,7 @@ async function handleAPI(req, res) {
|
|
|
428
528
|
return
|
|
429
529
|
}
|
|
430
530
|
|
|
431
|
-
// DELETE /api/trash/:cid —
|
|
531
|
+
// DELETE /api/trash/:cid — 永久删除回收站文件
|
|
432
532
|
if (pathname.match(/^\/api\/trash\/[^/]+$/) && method === 'DELETE') {
|
|
433
533
|
const cid = pathname.split('/')[3]
|
|
434
534
|
const result = await engine.permanentDeleteTrashFile(cid)
|
|
@@ -436,14 +536,14 @@ async function handleAPI(req, res) {
|
|
|
436
536
|
return
|
|
437
537
|
}
|
|
438
538
|
|
|
439
|
-
// DELETE /api/trash —
|
|
539
|
+
// DELETE /api/trash — 清空回收站
|
|
440
540
|
if (pathname === '/api/trash' && method === 'DELETE') {
|
|
441
541
|
const result = await engine.emptyTrash()
|
|
442
542
|
json({ success: true, trashFiles: result })
|
|
443
543
|
return
|
|
444
544
|
}
|
|
445
545
|
|
|
446
|
-
// POST /api/files/:cid/star —
|
|
546
|
+
// POST /api/files/:cid/star — 切换星标状态
|
|
447
547
|
if (pathname.match(/^\/api\/files\/[^/]+\/star$/) && method === 'POST') {
|
|
448
548
|
const cid = pathname.split('/')[3]
|
|
449
549
|
try {
|
|
@@ -455,7 +555,7 @@ async function handleAPI(req, res) {
|
|
|
455
555
|
return
|
|
456
556
|
}
|
|
457
557
|
|
|
458
|
-
// GET /api/storage —
|
|
558
|
+
// GET /api/storage — 获取存储统计信息
|
|
459
559
|
if (pathname === '/api/storage' && method === 'GET') {
|
|
460
560
|
const result = await engine.getStorageStats()
|
|
461
561
|
json(result)
|
|
@@ -469,79 +569,29 @@ async function handleAPI(req, res) {
|
|
|
469
569
|
}
|
|
470
570
|
}
|
|
471
571
|
|
|
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
572
|
function wsBroadcast(event, data) {
|
|
513
573
|
const payload = JSON.stringify({ event, data })
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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 {}
|
|
574
|
+
if (wss) {
|
|
575
|
+
wss.clients.forEach((client) => {
|
|
576
|
+
if (client.readyState === 1) {
|
|
577
|
+
try { client.send(payload) } catch {}
|
|
578
|
+
}
|
|
579
|
+
})
|
|
538
580
|
}
|
|
539
581
|
}
|
|
540
582
|
|
|
541
|
-
// ---
|
|
583
|
+
// --- 主函数 ---
|
|
542
584
|
async function main() {
|
|
543
585
|
console.log('[MostBox] Starting core daemon...')
|
|
544
586
|
|
|
587
|
+
if (fs.existsSync(UPLOAD_TMP_DIR)) {
|
|
588
|
+
const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
|
|
589
|
+
for (const file of staleFiles) {
|
|
590
|
+
try { fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file)) } catch {}
|
|
591
|
+
}
|
|
592
|
+
console.log(`[MostBox] Cleaned ${staleFiles.length} stale upload temp files`)
|
|
593
|
+
}
|
|
594
|
+
|
|
545
595
|
const dataPath = getDataPath()
|
|
546
596
|
console.log(`[MostBox] Storage: ${dataPath}`)
|
|
547
597
|
|
|
@@ -561,7 +611,8 @@ async function main() {
|
|
|
561
611
|
console.log('[MostBox] Engine ready')
|
|
562
612
|
|
|
563
613
|
serverInstance = http.createServer((req, res) => {
|
|
564
|
-
|
|
614
|
+
const allowedOrigin = `http://${HOST}:${PORT}`
|
|
615
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin)
|
|
565
616
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
566
617
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
|
567
618
|
|
|
@@ -571,16 +622,37 @@ async function main() {
|
|
|
571
622
|
return
|
|
572
623
|
}
|
|
573
624
|
|
|
625
|
+
const clientIp = req.socket.remoteAddress || 'unknown'
|
|
626
|
+
|
|
627
|
+
if (!checkRateLimit(clientIp)) {
|
|
628
|
+
res.writeHead(429, { 'Content-Type': 'application/json' })
|
|
629
|
+
res.end(JSON.stringify({ error: 'Too many requests' }))
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
|
|
574
633
|
if (req.url.startsWith('/api/')) {
|
|
575
|
-
handleAPI(req, res)
|
|
634
|
+
handleAPI(req, res).catch(err => {
|
|
635
|
+
console.error('[Unhandled API Error]', err)
|
|
636
|
+
if (!res.headersSent) {
|
|
637
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
638
|
+
res.end(JSON.stringify({ error: 'Internal server error' }))
|
|
639
|
+
}
|
|
640
|
+
})
|
|
576
641
|
} else {
|
|
577
642
|
serveStatic(req, res)
|
|
578
643
|
}
|
|
579
644
|
})
|
|
580
645
|
|
|
581
|
-
|
|
646
|
+
wss = new WebSocketServer({ noServer: true })
|
|
647
|
+
wss.on('connection', (ws) => {
|
|
648
|
+
ws.on('error', () => {})
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
serverInstance.on('upgrade', (req, socket, head) => {
|
|
582
652
|
if (req.url.startsWith('/ws')) {
|
|
583
|
-
|
|
653
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
654
|
+
wss.emit('connection', ws, req)
|
|
655
|
+
})
|
|
584
656
|
} else {
|
|
585
657
|
socket.destroy()
|
|
586
658
|
}
|
|
@@ -590,20 +662,31 @@ async function main() {
|
|
|
590
662
|
const url = `http://${HOST}:${PORT}`
|
|
591
663
|
console.log(`[MostBox] Server running at ${url}`)
|
|
592
664
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
665
|
+
if (process.platform === 'win32') {
|
|
666
|
+
spawn('cmd.exe', ['/c', 'start', '', url], {
|
|
667
|
+
detached: true,
|
|
668
|
+
stdio: 'ignore'
|
|
669
|
+
}).unref()
|
|
670
|
+
} else {
|
|
671
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
672
|
+
spawn(cmd, [url], {
|
|
673
|
+
detached: true,
|
|
674
|
+
stdio: 'ignore'
|
|
675
|
+
}).unref()
|
|
676
|
+
}
|
|
596
677
|
})
|
|
597
678
|
|
|
598
679
|
process.on('SIGINT', async () => {
|
|
599
680
|
console.log('\n[MostBox] Shutting down...')
|
|
600
681
|
await engine.stop()
|
|
682
|
+
if (wss) wss.close()
|
|
601
683
|
serverInstance.close()
|
|
602
684
|
process.exit(0)
|
|
603
685
|
})
|
|
604
686
|
|
|
605
687
|
process.on('SIGTERM', async () => {
|
|
606
688
|
await engine.stop()
|
|
689
|
+
if (wss) wss.close()
|
|
607
690
|
serverInstance.close()
|
|
608
691
|
process.exit(0)
|
|
609
692
|
})
|