most-box 0.0.2 → 0.0.6

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.
Files changed (181) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +222 -156
  3. package/electron/main.js +67 -0
  4. package/out/404/index.html +15 -0
  5. package/out/404.html +15 -0
  6. package/out/__next.__PAGE__.txt +11 -0
  7. package/out/__next._full.txt +21 -0
  8. package/out/__next._head.txt +5 -0
  9. package/out/__next._index.txt +7 -0
  10. package/out/__next._tree.txt +4 -0
  11. package/out/_next/static/alUUgRz4oMlw4EtULOYfV/_buildManifest.js +11 -0
  12. package/out/_next/static/alUUgRz4oMlw4EtULOYfV/_clientMiddlewareManifest.js +1 -0
  13. package/out/_next/static/alUUgRz4oMlw4EtULOYfV/_ssgManifest.js +1 -0
  14. package/out/_next/static/chunks/00s106sbq8t9v.js +1 -0
  15. package/out/_next/static/chunks/0174xh3wfsjm1.js +2 -0
  16. package/out/_next/static/chunks/01xlw8hd842-c.js +1 -0
  17. package/out/_next/static/chunks/02ou_44kkb5dz.js +1 -0
  18. package/out/_next/static/chunks/02pr2b_eos3~h.js +1 -0
  19. package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
  20. package/out/_next/static/chunks/07lsjkarm1p9f.css +1 -0
  21. package/out/_next/static/chunks/0_-ccbcyh_o30.css +1 -0
  22. package/out/_next/static/chunks/0_b839~4.q324.js +1 -0
  23. package/out/_next/static/chunks/0_sna3wdypbzr.js +1 -0
  24. package/out/_next/static/chunks/0_wia9ofmsi1c.css +2 -0
  25. package/out/_next/static/chunks/0byj66sc-9o0g.js +1 -0
  26. package/out/_next/static/chunks/0bzupvr5gt3k9.js +31 -0
  27. package/out/_next/static/chunks/0d3shmwh5_nmn.js +1 -0
  28. package/out/_next/static/chunks/0du450zbk4kq_.js +1 -0
  29. package/out/_next/static/chunks/0e_h0d3ekzks8.css +1 -0
  30. package/out/_next/static/chunks/0ho~log~~-jwp.css +1 -0
  31. package/out/_next/static/chunks/0ibjp~7qzxfjv.js +5 -0
  32. package/out/_next/static/chunks/0imvn_arv36xt.css +1 -0
  33. package/out/_next/static/chunks/0j9~17180dl8j.js +1 -0
  34. package/out/_next/static/chunks/0ji.28mehrvdp.js +1 -0
  35. package/out/_next/static/chunks/0jl~j62iz2uvr.js +1 -0
  36. package/out/_next/static/chunks/0nct0fubs64d-.js +1 -0
  37. package/out/_next/static/chunks/0n~dq4kpx9xxx.js +1 -0
  38. package/out/_next/static/chunks/0pqt~8bl3ukh4.js +4 -0
  39. package/out/_next/static/chunks/0q7ck9f.90_i9.js +1 -0
  40. package/out/_next/static/chunks/0qub_r0x_r-e9.css +1 -0
  41. package/out/_next/static/chunks/0rr4gwjp9z~9a.js +1 -0
  42. package/out/_next/static/chunks/0ry.po.a~iu4p.js +1 -0
  43. package/out/_next/static/chunks/0slwj0c46k5cu.js +1 -0
  44. package/out/_next/static/chunks/0sorqk.oc6b7j.css +1 -0
  45. package/out/_next/static/chunks/11dalasm30arx.js +1 -0
  46. package/out/_next/static/chunks/turbopack-0a_g3u0ud~jb8.js +1 -0
  47. package/out/_not-found/__next._full.txt +20 -0
  48. package/out/_not-found/__next._head.txt +5 -0
  49. package/out/_not-found/__next._index.txt +7 -0
  50. package/out/_not-found/__next._not-found.__PAGE__.txt +9 -0
  51. package/out/_not-found/__next._not-found.txt +5 -0
  52. package/out/_not-found/__next._tree.txt +2 -0
  53. package/out/_not-found/index.html +15 -0
  54. package/out/_not-found/index.txt +20 -0
  55. package/out/app/__next._full.txt +20 -0
  56. package/out/app/__next._head.txt +5 -0
  57. package/out/app/__next._index.txt +7 -0
  58. package/out/app/__next._tree.txt +2 -0
  59. package/out/app/__next.app.__PAGE__.txt +9 -0
  60. package/out/app/__next.app.txt +5 -0
  61. package/out/app/index.html +15 -0
  62. package/out/app/index.txt +20 -0
  63. package/out/changelog/__next._full.txt +22 -0
  64. package/out/changelog/__next._head.txt +5 -0
  65. package/out/changelog/__next._index.txt +7 -0
  66. package/out/changelog/__next._tree.txt +3 -0
  67. package/out/changelog/__next.changelog.__PAGE__.txt +10 -0
  68. package/out/changelog/__next.changelog.txt +5 -0
  69. package/out/changelog/index.html +15 -0
  70. package/out/changelog/index.txt +22 -0
  71. package/out/chat/__next._full.txt +21 -0
  72. package/out/chat/__next._head.txt +5 -0
  73. package/out/chat/__next._index.txt +7 -0
  74. package/out/chat/__next._tree.txt +3 -0
  75. package/out/chat/__next.chat.__PAGE__.txt +9 -0
  76. package/out/chat/__next.chat.txt +6 -0
  77. package/out/chat/index.html +15 -0
  78. package/out/chat/index.txt +21 -0
  79. package/out/docs/__next._full.txt +22 -0
  80. package/out/docs/__next._head.txt +5 -0
  81. package/out/docs/__next._index.txt +7 -0
  82. package/out/docs/__next._tree.txt +3 -0
  83. package/out/docs/__next.docs.__PAGE__.txt +10 -0
  84. package/out/docs/__next.docs.txt +5 -0
  85. package/out/docs/getting-started/__next._full.txt +22 -0
  86. package/out/docs/getting-started/__next._head.txt +5 -0
  87. package/out/docs/getting-started/__next._index.txt +7 -0
  88. package/out/docs/getting-started/__next._tree.txt +3 -0
  89. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +10 -0
  90. package/out/docs/getting-started/__next.docs.getting-started.txt +5 -0
  91. package/out/docs/getting-started/__next.docs.txt +5 -0
  92. package/out/docs/getting-started/index.html +15 -0
  93. package/out/docs/getting-started/index.txt +22 -0
  94. package/out/docs/index.html +15 -0
  95. package/out/docs/index.txt +22 -0
  96. package/out/download/__next._full.txt +34 -0
  97. package/out/download/__next._head.txt +5 -0
  98. package/out/download/__next._index.txt +7 -0
  99. package/out/download/__next._tree.txt +4 -0
  100. package/out/download/__next.download.__PAGE__.txt +16 -0
  101. package/out/download/__next.download.txt +5 -0
  102. package/out/download/index.html +15 -0
  103. package/out/download/index.txt +34 -0
  104. package/out/favicon.ico +0 -0
  105. package/out/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  106. package/out/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  107. package/out/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
  108. package/out/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
  109. package/out/index.html +15 -0
  110. package/out/index.txt +21 -0
  111. package/out/lottery/__next._full.txt +21 -0
  112. package/out/lottery/__next._head.txt +5 -0
  113. package/out/lottery/__next._index.txt +7 -0
  114. package/out/lottery/__next._tree.txt +3 -0
  115. package/out/lottery/__next.lottery.__PAGE__.txt +9 -0
  116. package/out/lottery/__next.lottery.txt +6 -0
  117. package/out/lottery/index.html +15 -0
  118. package/out/lottery/index.txt +21 -0
  119. package/out/ping/__next._full.txt +21 -0
  120. package/out/ping/__next._head.txt +5 -0
  121. package/out/ping/__next._index.txt +7 -0
  122. package/out/ping/__next._tree.txt +4 -0
  123. package/out/ping/__next.ping.__PAGE__.txt +10 -0
  124. package/out/ping/__next.ping.txt +5 -0
  125. package/out/ping/index.html +15 -0
  126. package/out/ping/index.txt +21 -0
  127. package/out/pwa-512x512.png +0 -0
  128. package/out/web3/__next._full.txt +21 -0
  129. package/out/web3/__next._head.txt +5 -0
  130. package/out/web3/__next._index.txt +7 -0
  131. package/out/web3/__next._tree.txt +3 -0
  132. package/out/web3/__next.web3.__PAGE__.txt +9 -0
  133. package/out/web3/__next.web3.txt +6 -0
  134. package/out/web3/ed25519/__next._full.txt +20 -0
  135. package/out/web3/ed25519/__next._head.txt +5 -0
  136. package/out/web3/ed25519/__next._index.txt +7 -0
  137. package/out/web3/ed25519/__next._tree.txt +3 -0
  138. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +6 -0
  139. package/out/web3/ed25519/__next.web3.ed25519.txt +5 -0
  140. package/out/web3/ed25519/__next.web3.txt +6 -0
  141. package/out/web3/ed25519/index.html +1 -0
  142. package/out/web3/ed25519/index.txt +20 -0
  143. package/out/web3/index.html +15 -0
  144. package/out/web3/index.txt +21 -0
  145. package/out/web3/tools/__next._full.txt +20 -0
  146. package/out/web3/tools/__next._head.txt +5 -0
  147. package/out/web3/tools/__next._index.txt +7 -0
  148. package/out/web3/tools/__next._tree.txt +3 -0
  149. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +6 -0
  150. package/out/web3/tools/__next.web3.tools.txt +5 -0
  151. package/out/web3/tools/__next.web3.txt +6 -0
  152. package/out/web3/tools/index.html +1 -0
  153. package/out/web3/tools/index.txt +20 -0
  154. package/package.json +162 -48
  155. package/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  156. package/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  157. package/public/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
  158. package/public/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
  159. package/public/pwa-512x512.png +0 -0
  160. package/server/cli.js +3 -0
  161. package/server/index.js +963 -0
  162. package/{src → server/src}/config.js +51 -39
  163. package/{src → server/src}/core/cid.js +157 -146
  164. package/{src → server/src}/index.js +1950 -1201
  165. package/server/src/utils/api.js +68 -0
  166. package/server/src/utils/avatar.js +11 -0
  167. package/{src → server/src}/utils/errors.js +70 -66
  168. package/server/src/utils/mostWallet.js +42 -0
  169. package/server/src/utils/mp.js +117 -0
  170. package/{src → server/src}/utils/security.js +173 -169
  171. package/server/src/utils/userIdentity.js +93 -0
  172. package/build.mjs +0 -40
  173. package/cli.js +0 -2
  174. package/public/app.css +0 -1519
  175. package/public/app.jsx +0 -1543
  176. package/public/bundle.css +0 -1
  177. package/public/bundle.js +0 -107
  178. package/public/error-boundary.jsx +0 -50
  179. package/public/index.html +0 -16
  180. package/public/index.jsx +0 -20
  181. package/server.js +0 -698
@@ -0,0 +1,963 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import os from 'node:os'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { spawn } from 'node:child_process'
6
+ import Busboy from 'busboy'
7
+ import { WebSocketServer } from 'ws'
8
+ import { Hono } from 'hono'
9
+ import { cors } from 'hono/cors'
10
+ import { serveStatic } from '@hono/node-server/serve-static'
11
+ import { serve } from '@hono/node-server'
12
+ import { MostBoxEngine } from './src/index.js'
13
+ import { parseMostLink, validateCidString } from './src/core/cid.js'
14
+ import { sanitizeFilename } from './src/utils/security.js'
15
+ import { MAX_FILE_SIZE } from './src/config.js'
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
+ const PORT = Number(process.env.MOSTBOX_PORT || process.env.PORT) || 1976
19
+ const HOST = process.env.MOSTBOX_HOST || '0.0.0.0'
20
+
21
+ const MAX_UPLOAD_SIZE = MAX_FILE_SIZE
22
+ const UPLOAD_TMP_DIR = path.join(os.tmpdir(), 'most-box-uploads')
23
+
24
+ const RATE_LIMIT_WINDOW = 60 * 1000
25
+ const RATE_LIMIT_MAX_REQUESTS = 120
26
+
27
+ // --- 配置 ---
28
+ const CONFIG_DIR = path.join(os.homedir(), '.most-box')
29
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
30
+
31
+ function loadConfig() {
32
+ try {
33
+ if (fs.existsSync(CONFIG_FILE)) {
34
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'))
35
+ }
36
+ } catch (err) {
37
+ console.error('[Config] Load error:', err.message)
38
+ }
39
+ return {}
40
+ }
41
+
42
+ function saveConfig(config) {
43
+ try {
44
+ if (!fs.existsSync(CONFIG_DIR)) {
45
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
46
+ }
47
+ const tmpPath = CONFIG_FILE + '.tmp'
48
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
49
+ fs.renameSync(tmpPath, CONFIG_FILE)
50
+ return true
51
+ } catch (err) {
52
+ console.error('[Config] Save error:', err.message)
53
+ return false
54
+ }
55
+ }
56
+
57
+ function getDataPath() {
58
+ if (process.env.MOSTBOX_DATA_PATH) {
59
+ return process.env.MOSTBOX_DATA_PATH
60
+ }
61
+ const config = loadConfig()
62
+ return config.dataPath || path.join(os.homedir(), 'most-data')
63
+ }
64
+
65
+ // --- 静态文件服务 ---
66
+ const MIME_TYPES = {
67
+ '.html': 'text/html; charset=utf-8',
68
+ '.js': 'application/javascript; charset=utf-8',
69
+ '.css': 'text/css; charset=utf-8',
70
+ '.json': 'application/json',
71
+ '.png': 'image/png',
72
+ '.jpg': 'image/jpeg',
73
+ '.jpeg': 'image/jpeg',
74
+ '.gif': 'image/gif',
75
+ '.webp': 'image/webp',
76
+ '.svg': 'image/svg+xml',
77
+ '.ico': 'image/x-icon',
78
+ '.mp4': 'video/mp4',
79
+ '.webm': 'video/webm',
80
+ '.ogg': 'video/ogg',
81
+ '.mp3': 'audio/mpeg',
82
+ '.wav': 'audio/wav',
83
+ '.flac': 'audio/flac',
84
+ '.aac': 'audio/aac',
85
+ '.m4a': 'audio/mp4',
86
+ '.opus': 'audio/opus',
87
+ '.woff2': 'font/woff2',
88
+ '.woff': 'font/woff',
89
+ }
90
+
91
+ function getMimeType(fileName) {
92
+ const ext = path.extname(fileName).toLowerCase()
93
+ return MIME_TYPES[ext] || 'application/octet-stream'
94
+ }
95
+
96
+ function decodeFilenameFromHeader(headerStr) {
97
+ if (!headerStr) return null
98
+
99
+ const filenameStarMatch = headerStr.match(
100
+ /filename\*=(?:UTF-8''|utf-8'')([^;\r\n]+)/i
101
+ )
102
+ if (filenameStarMatch) {
103
+ return decodeURIComponent(filenameStarMatch[1])
104
+ }
105
+
106
+ const filenameMatch = headerStr.match(/filename="([^"]+)"/)
107
+ if (filenameMatch) {
108
+ const rawFilename = filenameMatch[1]
109
+ try {
110
+ const buf = Buffer.from(rawFilename, 'latin1')
111
+ const decoded = buf.toString('utf8')
112
+ if (decoded.includes('\ufffd')) {
113
+ return rawFilename
114
+ }
115
+ return decoded
116
+ } catch {
117
+ return rawFilename
118
+ }
119
+ }
120
+
121
+ const filenamePlainMatch = headerStr.match(/filename=([^;\r\n]+)/)
122
+ if (filenamePlainMatch) {
123
+ return filenamePlainMatch[1].trim()
124
+ }
125
+ return null
126
+ }
127
+
128
+ async function parseMultipartBusboy(req) {
129
+ return new Promise((resolve, reject) => {
130
+ if (!fs.existsSync(UPLOAD_TMP_DIR)) {
131
+ fs.mkdirSync(UPLOAD_TMP_DIR, { recursive: true })
132
+ }
133
+
134
+ const busboy = Busboy({
135
+ headers: req.headers,
136
+ limits: {
137
+ fileSize: MAX_UPLOAD_SIZE,
138
+ files: 1,
139
+ fields: 0,
140
+ },
141
+ })
142
+
143
+ const result = { filePath: null, filename: null }
144
+ let fileSize = 0
145
+ let writeStream = null
146
+ let tempPath = null
147
+
148
+ busboy.on('file', (name, stream, info) => {
149
+ result.filename = decodeFilenameFromHeader(`filename="${info.filename}"`)
150
+ tempPath = path.join(
151
+ UPLOAD_TMP_DIR,
152
+ `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
153
+ )
154
+ writeStream = fs.createWriteStream(tempPath)
155
+
156
+ stream.on('data', chunk => {
157
+ fileSize += chunk.length
158
+ if (fileSize > MAX_UPLOAD_SIZE) {
159
+ stream.destroy()
160
+ writeStream.destroy()
161
+ fs.unlink(tempPath, () => {})
162
+ reject(new Error('File too large'))
163
+ return
164
+ }
165
+ })
166
+
167
+ stream.on('error', () => {
168
+ if (tempPath) fs.unlink(tempPath, () => {})
169
+ })
170
+
171
+ stream.pipe(writeStream)
172
+
173
+ writeStream.on('finish', () => {
174
+ result.filePath = tempPath
175
+ resolve(result)
176
+ })
177
+
178
+ writeStream.on('error', err => {
179
+ if (tempPath) fs.unlink(tempPath, () => {})
180
+ reject(err)
181
+ })
182
+ })
183
+
184
+ busboy.on('error', err => {
185
+ if (tempPath) fs.unlink(tempPath, () => {})
186
+ reject(err)
187
+ })
188
+
189
+ busboy.on('close', () => {
190
+ if (!result.filename) {
191
+ resolve(null)
192
+ }
193
+ })
194
+
195
+ req.on('error', err => {
196
+ if (tempPath) fs.unlink(tempPath, () => {})
197
+ reject(err)
198
+ })
199
+ req.pipe(busboy)
200
+ })
201
+ }
202
+
203
+ // --- Hono 应用工厂 ---
204
+ export function createApp(engine, options = {}) {
205
+ const appPort = options.port || PORT
206
+ const wssRef = options.wssRef || { current: null }
207
+ const serverInstanceRef = options.serverInstanceRef || { current: null }
208
+
209
+ // 速率限制(每个 app 实例独立)
210
+ const rateLimitMap = new Map()
211
+ function checkRateLimit(clientIp) {
212
+ const now = Date.now()
213
+ if (!rateLimitMap.has(clientIp)) {
214
+ rateLimitMap.set(clientIp, [])
215
+ }
216
+ const requests = rateLimitMap.get(clientIp)
217
+ while (requests.length > 0 && requests[0] < now - RATE_LIMIT_WINDOW) {
218
+ requests.shift()
219
+ }
220
+ if (requests.length === 0) {
221
+ rateLimitMap.delete(clientIp)
222
+ }
223
+ if (requests.length >= RATE_LIMIT_MAX_REQUESTS) {
224
+ return false
225
+ }
226
+ requests.push(now)
227
+ return true
228
+ }
229
+
230
+ function rateLimitMiddleware() {
231
+ return async (c, next) => {
232
+ const clientIp =
233
+ c.req.header('x-forwarded-for') ||
234
+ c.env.incoming?.socket?.remoteAddress ||
235
+ 'unknown'
236
+ if (!checkRateLimit(clientIp)) {
237
+ return c.json({ error: 'Too many requests' }, 429)
238
+ }
239
+ await next()
240
+ }
241
+ }
242
+
243
+ // WebSocket 广播
244
+ const channelSubscriptions = new Map()
245
+
246
+ function wsBroadcast(event, data) {
247
+ const payload = JSON.stringify({ event, data })
248
+ const wss = wssRef.current
249
+ if (wss) {
250
+ wss.clients.forEach(client => {
251
+ if (client.readyState === 1) {
252
+ try {
253
+ client.send(payload)
254
+ } catch {}
255
+ }
256
+ })
257
+ }
258
+ }
259
+
260
+ function wsSendToChannel(channelName, event, data) {
261
+ const payload = JSON.stringify({ event, data })
262
+ const subscribers = channelSubscriptions.get(channelName)
263
+ if (subscribers) {
264
+ subscribers.forEach(ws => {
265
+ if (ws.readyState === 1) {
266
+ try {
267
+ ws.send(payload)
268
+ } catch {}
269
+ }
270
+ })
271
+ }
272
+ }
273
+
274
+ function subscribeToChannel(ws, channelName) {
275
+ if (!channelSubscriptions.has(channelName)) {
276
+ channelSubscriptions.set(channelName, new Set())
277
+ }
278
+ channelSubscriptions.get(channelName).add(ws)
279
+ }
280
+
281
+ function unsubscribeFromChannel(ws, channelName) {
282
+ const subscribers = channelSubscriptions.get(channelName)
283
+ if (subscribers) {
284
+ subscribers.delete(ws)
285
+ if (subscribers.size === 0) {
286
+ channelSubscriptions.delete(channelName)
287
+ }
288
+ }
289
+ }
290
+
291
+ function cleanupWsSubscriptions(ws) {
292
+ for (const [channel, subscribers] of channelSubscriptions) {
293
+ subscribers.delete(ws)
294
+ if (subscribers.size === 0) {
295
+ channelSubscriptions.delete(channel)
296
+ }
297
+ }
298
+ }
299
+
300
+ // 将广播函数挂载到 engine 上供外部测试使用
301
+ engine.wsBroadcast = wsBroadcast
302
+ engine.wsSendToChannel = wsSendToChannel
303
+
304
+ const app = new Hono()
305
+
306
+ // CORS 中间件
307
+ app.use(
308
+ '/api/*',
309
+ cors({
310
+ origin: [
311
+ 'http://localhost:3000',
312
+ 'https://most.box',
313
+ `http://localhost:${appPort}`,
314
+ ],
315
+ credentials: true,
316
+ })
317
+ )
318
+
319
+ // 速率限制中间件
320
+ app.use('/api/*', rateLimitMiddleware())
321
+
322
+ // 全局错误处理
323
+ app.onError((err, c) => {
324
+ console.error('[API Error]', err)
325
+ try {
326
+ fs.appendFileSync(
327
+ 'server-error.log',
328
+ `[${new Date().toISOString()}] ${err.stack}\n`
329
+ )
330
+ } catch {}
331
+ return c.json({ error: err.message, code: err.code }, 500)
332
+ })
333
+
334
+ // --- 配置路由 ---
335
+ app.get('/api/node-id', c => {
336
+ return c.json({ id: engine.getNodeId() })
337
+ })
338
+
339
+ app.get('/api/config', c => {
340
+ const config = loadConfig()
341
+ return c.json({ dataPath: config.dataPath || '' })
342
+ })
343
+
344
+ app.post('/api/config', async c => {
345
+ const body = await c.req.json()
346
+ const config = loadConfig()
347
+
348
+ if (body.resetStorage) {
349
+ config.dataPath = ''
350
+ } else if (body.dataPath !== undefined) {
351
+ let dataPath = body.dataPath.trim()
352
+ let basePath = dataPath
353
+
354
+ if (dataPath.match(/^[A-Za-z]:\\$/)) {
355
+ basePath = dataPath
356
+ dataPath = path.join(dataPath, 'most-data')
357
+ }
358
+
359
+ if (!fs.existsSync(basePath)) {
360
+ return c.json({ error: '目录不存在' }, 400)
361
+ }
362
+
363
+ if (!fs.existsSync(dataPath)) {
364
+ fs.mkdirSync(dataPath, { recursive: true })
365
+ }
366
+
367
+ config.dataPath = dataPath
368
+ }
369
+
370
+ const success = saveConfig(config)
371
+ return c.json({ success, dataPath: getDataPath() })
372
+ })
373
+
374
+ app.get('/api/config/data-path', c => {
375
+ const config = loadConfig()
376
+ const isDefault = !config.dataPath
377
+ const dataPath = getDataPath()
378
+ return c.json({ dataPath, isDefault })
379
+ })
380
+
381
+ // --- 网络路由 ---
382
+ app.get('/api/network-status', c => {
383
+ return c.json(engine.getNetworkStatus())
384
+ })
385
+
386
+ app.get('/api/network', c => {
387
+ const interfaces = os.networkInterfaces()
388
+ const addresses = []
389
+ const seen = new Set()
390
+
391
+ for (const [name, nets] of Object.entries(interfaces)) {
392
+ for (const net of nets) {
393
+ if (net.family !== 'IPv4' || net.internal) continue
394
+ if (seen.has(net.address)) continue
395
+ seen.add(net.address)
396
+
397
+ let type = 'lan'
398
+ let label = '局域网'
399
+ if (net.address.startsWith('100.')) {
400
+ type = 'tailscale'
401
+ label = 'Tailscale'
402
+ } else if (
403
+ name.toLowerCase().includes('zt') ||
404
+ name.toLowerCase().includes('zerotier')
405
+ ) {
406
+ type = 'zerotier'
407
+ label = 'ZeroTier'
408
+ }
409
+
410
+ addresses.push({ type, ip: net.address, label, iface: name })
411
+ }
412
+ }
413
+
414
+ const localEntry = {
415
+ type: 'local',
416
+ ip: 'localhost',
417
+ label: '本机',
418
+ iface: 'loopback',
419
+ }
420
+ return c.json({ port: appPort, addresses: [localEntry, ...addresses] })
421
+ })
422
+
423
+ // --- 文件路由 ---
424
+ app.get('/api/files', c => {
425
+ return c.json(engine.listPublishedFiles())
426
+ })
427
+
428
+ app.post('/api/publish', async c => {
429
+ const req = c.env.incoming
430
+ const result = await parseMultipartBusboy(req)
431
+
432
+ if (!result || !result.filename) {
433
+ return c.json({ error: 'No file provided' }, 400)
434
+ }
435
+
436
+ try {
437
+ const publishResult = await engine.publishFile(
438
+ result.filePath,
439
+ result.filename
440
+ )
441
+ return c.json({ success: true, ...publishResult })
442
+ } finally {
443
+ fs.unlink(result.filePath, () => {})
444
+ }
445
+ })
446
+
447
+ app.post('/api/download', async c => {
448
+ const body = await c.req.json()
449
+ if (!body.link) {
450
+ return c.json({ error: 'link is required' }, 400)
451
+ }
452
+
453
+ const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
454
+
455
+ const parsed = parseMostLink(body.link)
456
+ if (parsed.error) {
457
+ return c.json({ error: parsed.error }, 400)
458
+ }
459
+
460
+ const existingFile = engine
461
+ .getPublishedFiles()
462
+ .find(f => f.cid === parsed.cid)
463
+ if (existingFile) {
464
+ console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
465
+ return c.json({
466
+ success: true,
467
+ taskId,
468
+ alreadyExists: true,
469
+ fileName: existingFile.fileName,
470
+ })
471
+ }
472
+
473
+ engine.downloadFile(body.link, taskId).catch(err => {
474
+ if (err.message === 'Download cancelled') {
475
+ wsBroadcast('download:cancelled', { taskId })
476
+ } else {
477
+ wsBroadcast('download:error', { taskId, error: err.message })
478
+ }
479
+ })
480
+
481
+ return c.json({ success: true, taskId })
482
+ })
483
+
484
+ app.post('/api/download/cancel', async c => {
485
+ const body = await c.req.json()
486
+ if (!body.taskId) {
487
+ return c.json({ error: 'taskId is required' }, 400)
488
+ }
489
+ engine.cancelDownload(body.taskId)
490
+ return c.json({ success: true })
491
+ })
492
+
493
+ app.delete('/api/files/:cid', async c => {
494
+ const cid = c.req.param('cid')
495
+ const cidValidation = validateCidString(cid)
496
+ if (!cidValidation.valid) {
497
+ return c.json({ error: cidValidation.error }, 400)
498
+ }
499
+ const result = await engine.deletePublishedFile(cid)
500
+ return c.json(result)
501
+ })
502
+
503
+ app.post('/api/move', async c => {
504
+ const body = await c.req.json()
505
+ if (!body.cid || !body.newFileName) {
506
+ return c.json({ error: 'cid and newFileName are required' }, 400)
507
+ }
508
+ const cidValidation = validateCidString(body.cid)
509
+ if (!cidValidation.valid) {
510
+ return c.json({ error: cidValidation.error }, 400)
511
+ }
512
+ const cleanFileName = sanitizeFilename(body.newFileName)
513
+ if (
514
+ !cleanFileName ||
515
+ cleanFileName === 'unnamed' ||
516
+ body.newFileName.length > 255
517
+ ) {
518
+ return c.json({ error: 'Invalid filename' }, 400)
519
+ }
520
+ try {
521
+ const result = engine.moveFile(body.cid, cleanFileName)
522
+ return c.json({ success: true, ...result })
523
+ } catch (err) {
524
+ return c.json({ error: err.message }, 400)
525
+ }
526
+ })
527
+
528
+ app.get('/api/files/:cid/download', async c => {
529
+ const cid = c.req.param('cid')
530
+ const cidValidation = validateCidString(cid)
531
+ if (!cidValidation.valid) {
532
+ return c.json({ error: cidValidation.error }, 400)
533
+ }
534
+
535
+ const rangeHeader = c.req.header('range')
536
+
537
+ try {
538
+ if (rangeHeader) {
539
+ const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
540
+ if (rangeMatch) {
541
+ const start = parseInt(rangeMatch[1], 10)
542
+ const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
543
+ const offset = start
544
+ const limit = end !== undefined ? end - start + 1 : undefined
545
+
546
+ const result = await engine.readFileRaw(cid, { offset, limit })
547
+ const contentType = getMimeType(result.fileName)
548
+
549
+ c.header('Content-Type', contentType)
550
+ c.header('Content-Length', String(result.buffer.length))
551
+ c.header(
552
+ 'Content-Range',
553
+ `bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`
554
+ )
555
+ c.header('Accept-Ranges', 'bytes')
556
+ c.status(206)
557
+ return c.body(result.buffer)
558
+ }
559
+ }
560
+
561
+ const result = await engine.readFileRaw(cid)
562
+ const contentType = getMimeType(result.fileName)
563
+ c.header('Content-Type', contentType)
564
+ c.header('Content-Length', String(result.totalSize))
565
+ c.header('Accept-Ranges', 'bytes')
566
+ c.header(
567
+ 'Content-Disposition',
568
+ `inline; filename="${encodeURIComponent(result.fileName)}"`
569
+ )
570
+ return c.body(result.buffer)
571
+ } catch (err) {
572
+ if (err.message === 'File not found') {
573
+ return c.json({ error: err.message }, 404)
574
+ }
575
+ return c.json({ error: err.message }, 400)
576
+ }
577
+ })
578
+
579
+ // --- 回收站路由 ---
580
+ app.get('/api/trash', c => {
581
+ return c.json(engine.listTrashFiles())
582
+ })
583
+
584
+ app.post('/api/trash/:cid/restore', async c => {
585
+ const cid = c.req.param('cid')
586
+ const cidValidation = validateCidString(cid)
587
+ if (!cidValidation.valid) {
588
+ return c.json({ error: cidValidation.error }, 400)
589
+ }
590
+ try {
591
+ const result = engine.restoreTrashFile(cid)
592
+ return c.json({ success: true, files: result })
593
+ } catch (err) {
594
+ return c.json({ error: err.message }, 400)
595
+ }
596
+ })
597
+
598
+ app.delete('/api/trash/:cid', async c => {
599
+ const cid = c.req.param('cid')
600
+ const cidValidation = validateCidString(cid)
601
+ if (!cidValidation.valid) {
602
+ return c.json({ error: cidValidation.error }, 400)
603
+ }
604
+ const result = await engine.permanentDeleteTrashFile(cid)
605
+ return c.json({ success: true, trashFiles: result })
606
+ })
607
+
608
+ app.delete('/api/trash', async c => {
609
+ const result = await engine.emptyTrash()
610
+ return c.json({ success: true, trashFiles: result })
611
+ })
612
+
613
+ // --- 收藏路由 ---
614
+ app.post('/api/files/:cid/star', async c => {
615
+ const cid = c.req.param('cid')
616
+ const cidValidation = validateCidString(cid)
617
+ if (!cidValidation.valid) {
618
+ return c.json({ error: cidValidation.error }, 400)
619
+ }
620
+ try {
621
+ const result = engine.toggleStarred(cid)
622
+ return c.json({ success: true, ...result })
623
+ } catch (err) {
624
+ return c.json({ error: err.message }, 400)
625
+ }
626
+ })
627
+
628
+ // --- 存储路由 ---
629
+ app.get('/api/storage', async c => {
630
+ const result = await engine.getStorageStats()
631
+ return c.json(result)
632
+ })
633
+
634
+ // --- 显示名路由 ---
635
+ app.get('/api/display-name', c => {
636
+ return c.json({ displayName: engine.getDisplayName() })
637
+ })
638
+
639
+ app.post('/api/display-name', async c => {
640
+ const body = await c.req.json()
641
+ if (!body.name || !body.name.trim()) {
642
+ return c.json({ error: 'name is required' }, 400)
643
+ }
644
+ const success = engine.setDisplayName(body.name)
645
+ return c.json({ success, displayName: engine.getDisplayName() })
646
+ })
647
+
648
+ // --- 频道路由 ---
649
+ app.post('/api/channels', async c => {
650
+ const body = await c.req.json()
651
+ if (!body.name || !body.name.trim()) {
652
+ return c.json({ error: 'name is required' }, 400)
653
+ }
654
+ try {
655
+ const result = await engine.createChannel(
656
+ body.name.trim(),
657
+ body.type || 'personal'
658
+ )
659
+ return c.json({ success: true, ...result })
660
+ } catch (err) {
661
+ return c.json({ error: err.message }, 400)
662
+ }
663
+ })
664
+
665
+ app.get('/api/channels', c => {
666
+ return c.json(engine.listChannels())
667
+ })
668
+
669
+ app.delete('/api/channels/:name', async c => {
670
+ const name = c.req.param('name')
671
+ try {
672
+ const result = await engine.leaveChannel(name)
673
+ return c.json({ success: true, channels: result })
674
+ } catch (err) {
675
+ return c.json({ error: err.message }, 400)
676
+ }
677
+ })
678
+
679
+ app.get('/api/channels/:name/messages', async c => {
680
+ const name = c.req.param('name')
681
+ const limit = parseInt(c.req.query('limit') || '100', 10)
682
+ const offset = parseInt(c.req.query('offset') || '0', 10)
683
+ try {
684
+ const messages = await engine.getChannelMessages(name, { limit, offset })
685
+ return c.json(messages)
686
+ } catch (err) {
687
+ return c.json({ error: err.message }, 400)
688
+ }
689
+ })
690
+
691
+ app.post('/api/channels/:name/messages', async c => {
692
+ const name = c.req.param('name')
693
+ const body = await c.req.json()
694
+ if (!body.content || !body.content.trim()) {
695
+ return c.json({ error: 'content is required' }, 400)
696
+ }
697
+ if (!body.author || !body.authorName) {
698
+ return c.json({ error: 'author and authorName are required' }, 400)
699
+ }
700
+ try {
701
+ const message = await engine.sendMessage(
702
+ name,
703
+ body.content,
704
+ body.author,
705
+ body.authorName
706
+ )
707
+ return c.json({ success: true, message })
708
+ } catch (err) {
709
+ return c.json({ error: err.message }, 400)
710
+ }
711
+ })
712
+
713
+ app.get('/api/channels/:name/peers', c => {
714
+ return c.json(engine.getChannelPeers(c.req.param('name')))
715
+ })
716
+
717
+ // --- 文件夹重命名 ---
718
+ app.post('/api/folder/rename', async c => {
719
+ const body = await c.req.json()
720
+ if (!body.oldPath || !body.newPath) {
721
+ return c.json({ error: 'oldPath and newPath are required' }, 400)
722
+ }
723
+ if (body.oldPath.length > 500 || body.newPath.length > 500) {
724
+ return c.json({ error: 'Path too long' }, 400)
725
+ }
726
+ if (body.oldPath.includes('..') || body.newPath.includes('..')) {
727
+ return c.json({ error: 'Path traversal not allowed' }, 400)
728
+ }
729
+ try {
730
+ const result = engine.renameFolder(body.oldPath, body.newPath)
731
+ return c.json({ success: true, ...result })
732
+ } catch (err) {
733
+ return c.json({ error: err.message }, 400)
734
+ }
735
+ })
736
+
737
+ // --- 关机路由 ---
738
+ app.post('/api/shutdown', c => {
739
+ const clientIp = c.env.incoming?.socket?.remoteAddress || 'unknown'
740
+ const isLocalhost =
741
+ clientIp === 'localhost' ||
742
+ clientIp === '::1' ||
743
+ clientIp === '::ffff:localhost'
744
+ if (!isLocalhost) {
745
+ return c.json({ error: 'Forbidden' }, 403)
746
+ }
747
+ c.json({ success: true })
748
+ console.log('[MostBox] Shutdown requested via API...')
749
+ setTimeout(async () => {
750
+ await engine.stop()
751
+ if (serverInstanceRef.current) serverInstanceRef.current.close()
752
+ console.log('[MostBox] Server stopped.')
753
+ process.exit(0)
754
+ }, 100)
755
+ return c.body(null)
756
+ })
757
+
758
+ // --- 静态文件服务(SPA fallback) ---
759
+ const publicDir = path.join(__dirname, '..', 'out')
760
+
761
+ app.get('/static/*', serveStatic({ root: './out' }))
762
+ app.get('/_next/*', serveStatic({ root: './out' }))
763
+
764
+ app.all('/api/*', c => {
765
+ return c.json({ error: 'Not found' }, 404)
766
+ })
767
+
768
+ app.get('*', async c => {
769
+ const pathname = c.req.path
770
+ const filePath = path.join(publicDir, pathname)
771
+
772
+ if (fs.existsSync(filePath)) {
773
+ const stat = fs.statSync(filePath)
774
+ if (stat.isFile()) {
775
+ const ext = path.extname(filePath)
776
+ c.header('Content-Type', MIME_TYPES[ext] || 'application/octet-stream')
777
+ return c.body(fs.readFileSync(filePath))
778
+ }
779
+ if (stat.isDirectory()) {
780
+ const dirIndex = path.join(filePath, 'index.html')
781
+ if (fs.existsSync(dirIndex)) {
782
+ c.header('Content-Type', 'text/html; charset=utf-8')
783
+ return c.body(fs.readFileSync(dirIndex, 'utf-8'))
784
+ }
785
+ }
786
+ }
787
+
788
+ const indexPath = path.join(publicDir, 'index.html')
789
+ if (fs.existsSync(indexPath)) {
790
+ c.header('Content-Type', 'text/html; charset=utf-8')
791
+ return c.body(fs.readFileSync(indexPath, 'utf-8'))
792
+ }
793
+
794
+ return c.json({ error: 'Not found' }, 404)
795
+ })
796
+
797
+ return {
798
+ app,
799
+ wsBroadcast,
800
+ wsSendToChannel,
801
+ subscribeToChannel,
802
+ unsubscribeFromChannel,
803
+ cleanupWsSubscriptions,
804
+ }
805
+ }
806
+
807
+ // --- 主函数 ---
808
+ export async function main() {
809
+ console.log('[MostBox] Starting core daemon...')
810
+
811
+ if (fs.existsSync(UPLOAD_TMP_DIR)) {
812
+ const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
813
+ for (const file of staleFiles) {
814
+ try {
815
+ fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file))
816
+ } catch {}
817
+ }
818
+ console.log(
819
+ `[MostBox] Cleaned ${staleFiles.length} stale upload temp files`
820
+ )
821
+ }
822
+
823
+ const dataPath = getDataPath()
824
+ console.log(`[MostBox] Storage: ${dataPath}`)
825
+
826
+ const engine = new MostBoxEngine({ dataPath })
827
+
828
+ const wssRef = { current: null }
829
+ const serverInstanceRef = { current: null }
830
+
831
+ const {
832
+ app,
833
+ wsBroadcast,
834
+ wsSendToChannel,
835
+ subscribeToChannel,
836
+ unsubscribeFromChannel,
837
+ cleanupWsSubscriptions,
838
+ } = createApp(engine, {
839
+ port: PORT,
840
+ wssRef,
841
+ serverInstanceRef,
842
+ })
843
+
844
+ engine.on('download:progress', data => wsBroadcast('download:progress', data))
845
+ engine.on('download:status', data => wsBroadcast('download:status', data))
846
+ engine.on('download:success', data => wsBroadcast('download:success', data))
847
+ engine.on('download:cancelled', data =>
848
+ wsBroadcast('download:cancelled', data)
849
+ )
850
+ engine.on('publish:progress', data => wsBroadcast('publish:progress', data))
851
+ engine.on('publish:success', data => wsBroadcast('publish:success', data))
852
+ engine.on('connection', () => {
853
+ wsBroadcast('network:status', engine.getNetworkStatus())
854
+ })
855
+ engine.on('channel:message', data =>
856
+ wsSendToChannel(data.channel, 'channel:message', data)
857
+ )
858
+ engine.on('channel:peer:online', data =>
859
+ wsBroadcast('channel:peer:online', data)
860
+ )
861
+ engine.on('channel:peer:offline', data =>
862
+ wsBroadcast('channel:peer:offline', data)
863
+ )
864
+ engine.on('channel:joined', data => wsBroadcast('channel:joined', data))
865
+ engine.on('channel:left', data => wsBroadcast('channel:left', data))
866
+
867
+ await engine.start()
868
+ console.log('[MostBox] Engine ready')
869
+
870
+ serverInstanceRef.current = serve(
871
+ { fetch: app.fetch, port: PORT, hostname: HOST },
872
+ () => {
873
+ const displayUrl = `http://localhost:${PORT}`
874
+ console.log(`[MostBox] Server running at ${displayUrl}`)
875
+
876
+ if (process.env.ELECTRON_APP) return
877
+
878
+ if (process.platform === 'win32') {
879
+ spawn('cmd.exe', ['/c', 'start', '', displayUrl], {
880
+ detached: true,
881
+ stdio: 'ignore',
882
+ }).unref()
883
+ } else {
884
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
885
+ spawn(cmd, [displayUrl], {
886
+ detached: true,
887
+ stdio: 'ignore',
888
+ }).unref()
889
+ }
890
+ }
891
+ )
892
+
893
+ wssRef.current = new WebSocketServer({ noServer: true })
894
+ wssRef.current.on('connection', ws => {
895
+ ws.on('error', () => {})
896
+ ws.on('close', () => {
897
+ cleanupWsSubscriptions(ws)
898
+ })
899
+ ws.on('message', raw => {
900
+ try {
901
+ const msg = JSON.parse(raw)
902
+ const { event, data } = msg
903
+
904
+ switch (event) {
905
+ case 'register':
906
+ ws.peerId = data.peerId
907
+ break
908
+ case 'channel:subscribe':
909
+ if (data.channel) {
910
+ subscribeToChannel(ws, data.channel)
911
+ }
912
+ break
913
+ case 'channel:unsubscribe':
914
+ if (data.channel) {
915
+ unsubscribeFromChannel(ws, data.channel)
916
+ }
917
+ break
918
+ }
919
+ } catch (err) {
920
+ console.error('[WS Message Error]', err.message)
921
+ }
922
+ })
923
+ })
924
+
925
+ serverInstanceRef.current.on('upgrade', (req, socket, head) => {
926
+ if (req.url.startsWith('/ws')) {
927
+ wssRef.current.handleUpgrade(req, socket, head, ws => {
928
+ wssRef.current.emit('connection', ws, req)
929
+ })
930
+ } else {
931
+ socket.destroy()
932
+ }
933
+ })
934
+
935
+ process.on('SIGINT', async () => {
936
+ console.log('\n[MostBox] Shutting down...')
937
+ await engine.stop()
938
+ if (wssRef.current) wssRef.current.close()
939
+ serverInstanceRef.current.close()
940
+ process.exit(0)
941
+ })
942
+
943
+ process.on('SIGTERM', async () => {
944
+ await engine.stop()
945
+ if (wssRef.current) wssRef.current.close()
946
+ serverInstanceRef.current.close()
947
+ process.exit(0)
948
+ })
949
+
950
+ return engine
951
+ }
952
+
953
+ // 仅在直接运行时执行 main(通过 node server/index.js 或 CLI)
954
+ const isMain =
955
+ process.argv[1] &&
956
+ (import.meta.url === new URL(process.argv[1], 'file://').href ||
957
+ fileURLToPath(import.meta.url) === path.resolve(process.argv[1]))
958
+ if (isMain) {
959
+ main().catch(err => {
960
+ console.error('[MostBox] Fatal error:', err)
961
+ process.exit(1)
962
+ })
963
+ }