most-box 0.1.1 → 0.1.3

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 (231) hide show
  1. package/README.md +39 -19
  2. package/electron/afterPack.cjs +87 -0
  3. package/electron/main.js +156 -8
  4. package/electron/preload.js +6 -0
  5. package/electron/updateChecker.js +97 -0
  6. package/electron/updateChecker.test.js +147 -0
  7. package/out/404/index.html +2 -2
  8. package/out/404.html +2 -2
  9. package/out/__next.__PAGE__.txt +6 -6
  10. package/out/__next._full.txt +16 -16
  11. package/out/__next._head.txt +3 -3
  12. package/out/__next._index.txt +8 -8
  13. package/out/__next._tree.txt +5 -5
  14. package/out/_next/static/chunks/04mo7rr..0_1q.js +1 -0
  15. package/out/_next/static/chunks/06rf3qq5ggs6v.js +1 -0
  16. package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
  17. package/out/_next/static/chunks/0_0oph_z1az14.js +1 -0
  18. package/out/_next/static/chunks/{0qou.u2e2dy48.css → 0adx~d-j05c9d.css} +2 -2
  19. package/out/_next/static/chunks/0cl7d~7abnk_p.css +1 -0
  20. package/out/_next/static/chunks/0d306t1wvjpdx.js +1 -0
  21. package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
  22. package/out/_next/static/chunks/{0o6lrkxy4jwag.js → 0gcsdf57gcm6h.js} +1 -1
  23. package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
  24. package/out/_next/static/chunks/0m_5nb6x8qy._.js +1 -0
  25. package/out/_next/static/chunks/0n.ayxmsar6e5.js +1 -0
  26. package/out/_next/static/chunks/{0usvo~vu7r8np.js → 0o9ce4cyf76by.js} +1 -1
  27. package/out/_next/static/chunks/0olqjomda37-e.js +1 -0
  28. package/out/_next/static/chunks/{0o98f1yq..o.8.js → 0pt.5cg1t09qs.js} +1 -1
  29. package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
  30. package/out/_next/static/chunks/0s~g.l~x049o2.js +1 -0
  31. package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
  32. package/out/_next/static/chunks/0voe1.ttrh84k.css +1 -0
  33. package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
  34. package/out/_next/static/chunks/0x.ky97owcxxs.js +1 -0
  35. package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
  36. package/out/_next/static/chunks/0ysj5b94vu4ri.js +1 -0
  37. package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
  38. package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
  39. package/out/_next/static/chunks/14_inksek_rth.js +2 -0
  40. package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
  41. package/out/_next/static/chunks/17cwkb2yn_akx.js +1 -0
  42. package/out/_next/static/chunks/184hxsuf-5c84.js +1 -0
  43. package/out/_next/static/chunks/{turbopack-0xs6mybc~5t_3.js → turbopack-0xta0kqwzkf28.js} +1 -1
  44. package/out/_not-found/__next._full.txt +13 -13
  45. package/out/_not-found/__next._head.txt +3 -3
  46. package/out/_not-found/__next._index.txt +8 -8
  47. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  48. package/out/_not-found/__next._not-found.txt +3 -3
  49. package/out/_not-found/__next._tree.txt +3 -3
  50. package/out/_not-found/index.html +2 -2
  51. package/out/_not-found/index.txt +13 -13
  52. package/out/admin/__next._full.txt +15 -15
  53. package/out/admin/__next._head.txt +3 -3
  54. package/out/admin/__next._index.txt +8 -8
  55. package/out/admin/__next._tree.txt +4 -4
  56. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  57. package/out/admin/__next.admin.txt +4 -4
  58. package/out/admin/index.html +2 -2
  59. package/out/admin/index.txt +15 -15
  60. package/out/app/__next._full.txt +14 -14
  61. package/out/app/__next._head.txt +3 -3
  62. package/out/app/__next._index.txt +8 -8
  63. package/out/app/__next._tree.txt +3 -3
  64. package/out/app/__next.app.__PAGE__.txt +4 -4
  65. package/out/app/__next.app.txt +3 -3
  66. package/out/app/index.html +2 -2
  67. package/out/app/index.txt +14 -14
  68. package/out/chat/__next._full.txt +15 -15
  69. package/out/chat/__next._head.txt +3 -3
  70. package/out/chat/__next._index.txt +8 -8
  71. package/out/chat/__next._tree.txt +4 -4
  72. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  73. package/out/chat/__next.chat.txt +4 -4
  74. package/out/chat/index.html +2 -2
  75. package/out/chat/index.txt +15 -15
  76. package/out/chat/join/__next._full.txt +25 -0
  77. package/out/chat/join/__next._head.txt +5 -0
  78. package/out/{changelog → chat/join}/__next._index.txt +8 -8
  79. package/out/chat/join/__next._tree.txt +5 -0
  80. package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
  81. package/out/chat/join/__next.chat.join.txt +5 -0
  82. package/out/chat/join/__next.chat.txt +5 -0
  83. package/out/chat/join/index.html +15 -0
  84. package/out/chat/join/index.txt +25 -0
  85. package/out/download/__next._full.txt +37 -33
  86. package/out/download/__next._head.txt +3 -3
  87. package/out/download/__next._index.txt +8 -8
  88. package/out/download/__next._tree.txt +5 -5
  89. package/out/download/__next.download.__PAGE__.txt +9 -14
  90. package/out/download/__next.download.txt +3 -3
  91. package/out/download/index.html +2 -2
  92. package/out/download/index.txt +37 -33
  93. package/out/favicon.ico +0 -0
  94. package/out/gandengyan/__next._full.txt +25 -0
  95. package/out/{changelog → gandengyan}/__next._head.txt +3 -3
  96. package/out/{docs → gandengyan}/__next._index.txt +8 -8
  97. package/out/gandengyan/__next._tree.txt +5 -0
  98. package/out/gandengyan/__next.gandengyan.__PAGE__.txt +10 -0
  99. package/out/gandengyan/__next.gandengyan.txt +5 -0
  100. package/out/gandengyan/index.html +15 -0
  101. package/out/gandengyan/index.txt +25 -0
  102. package/out/index.html +2 -2
  103. package/out/index.txt +16 -16
  104. package/out/note/__next._full.txt +14 -14
  105. package/out/note/__next._head.txt +3 -3
  106. package/out/note/__next._index.txt +8 -8
  107. package/out/note/__next._tree.txt +3 -3
  108. package/out/note/__next.note.__PAGE__.txt +4 -4
  109. package/out/note/__next.note.txt +3 -3
  110. package/out/note/index.html +2 -2
  111. package/out/note/index.txt +14 -14
  112. package/out/ping/__next._full.txt +16 -16
  113. package/out/ping/__next._head.txt +3 -3
  114. package/out/ping/__next._index.txt +8 -8
  115. package/out/ping/__next._tree.txt +5 -5
  116. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  117. package/out/ping/__next.ping.txt +4 -4
  118. package/out/ping/index.html +2 -2
  119. package/out/ping/index.txt +16 -16
  120. package/out/web3/__next._full.txt +15 -15
  121. package/out/web3/__next._head.txt +3 -3
  122. package/out/web3/__next._index.txt +8 -8
  123. package/out/web3/__next._tree.txt +4 -4
  124. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  125. package/out/web3/__next.web3.txt +4 -4
  126. package/out/web3/ed25519/__next._full.txt +13 -13
  127. package/out/web3/ed25519/__next._head.txt +3 -3
  128. package/out/web3/ed25519/__next._index.txt +8 -8
  129. package/out/web3/ed25519/__next._tree.txt +4 -4
  130. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  131. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  132. package/out/web3/ed25519/__next.web3.txt +4 -4
  133. package/out/web3/ed25519/index.html +1 -1
  134. package/out/web3/ed25519/index.txt +13 -13
  135. package/out/web3/index.html +2 -2
  136. package/out/web3/index.txt +15 -15
  137. package/out/web3/tools/__next._full.txt +13 -13
  138. package/out/web3/tools/__next._head.txt +3 -3
  139. package/out/web3/tools/__next._index.txt +8 -8
  140. package/out/web3/tools/__next._tree.txt +4 -4
  141. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  142. package/out/web3/tools/__next.web3.tools.txt +3 -3
  143. package/out/web3/tools/__next.web3.txt +4 -4
  144. package/out/web3/tools/index.html +1 -1
  145. package/out/web3/tools/index.txt +13 -13
  146. package/package.json +44 -35
  147. package/public/favicon.ico +0 -0
  148. package/server/index.js +142 -1304
  149. package/server/src/config.js +1 -1
  150. package/server/src/core/channelAttachment.js +68 -0
  151. package/server/src/core/cid.js +2 -88
  152. package/server/src/core/cidTopic.js +29 -0
  153. package/server/src/core/mostLink.js +88 -0
  154. package/server/src/games/gandengyan.js +675 -0
  155. package/server/src/http/access.js +127 -0
  156. package/server/src/http/app.js +1102 -0
  157. package/server/src/http/errors.js +35 -0
  158. package/server/src/http/nodeLogs.js +53 -0
  159. package/server/src/http/nodeStatus.js +146 -0
  160. package/server/src/http/staticFiles.js +84 -0
  161. package/server/src/http/uploads.js +114 -0
  162. package/server/src/index.js +799 -211
  163. package/server/src/node/config.js +38 -6
  164. package/server/src/utils/api.js +305 -14
  165. package/server/src/utils/auth.js +63 -0
  166. package/server/src/utils/dateTime.js +30 -0
  167. package/server/src/utils/downloadMessages.js +89 -0
  168. package/server/src/utils/errors.js +7 -0
  169. package/server/src/utils/mostWallet.js +151 -0
  170. package/server/src/utils/mp.js +2 -26
  171. package/server/src/utils/noteBackup.js +2 -5
  172. package/server/src/utils/noteUtils.js +11 -3
  173. package/server/src/utils/userIdentity.js +0 -1
  174. package/out/_next/static/chunks/00-u5nq76f0.j.js +0 -1
  175. package/out/_next/static/chunks/00fm8lijienf1.js +0 -1
  176. package/out/_next/static/chunks/00o9ht.f2qm00.css +0 -4
  177. package/out/_next/static/chunks/00zi-erhjrny2.js +0 -2
  178. package/out/_next/static/chunks/084xf0edl9sfo.js +0 -1
  179. package/out/_next/static/chunks/09f1gfke9m5wg.css +0 -1
  180. package/out/_next/static/chunks/09xyi6fpro_d-.css +0 -1
  181. package/out/_next/static/chunks/0_npg_pcoywti.js +0 -5
  182. package/out/_next/static/chunks/0_r_mk1~6bosc.js +0 -1
  183. package/out/_next/static/chunks/0arm0a6adt7cc.css +0 -1
  184. package/out/_next/static/chunks/0c9j3eq_14vv2.css +0 -1
  185. package/out/_next/static/chunks/0d4bueddmcnca.js +0 -1
  186. package/out/_next/static/chunks/0gtwvy1z9ksa7.css +0 -1
  187. package/out/_next/static/chunks/0ho~log~~-jwp.css +0 -1
  188. package/out/_next/static/chunks/0j27tcmtt4ly7.js +0 -1
  189. package/out/_next/static/chunks/0j3v4mq67wtnh.js +0 -1
  190. package/out/_next/static/chunks/0lkmf5ry.s_7w.js +0 -1
  191. package/out/_next/static/chunks/0p486m03-zfoi.js +0 -1
  192. package/out/_next/static/chunks/0r1~k82nji8sf.js +0 -1
  193. package/out/_next/static/chunks/0v7qp4hv-_._r.js +0 -1
  194. package/out/_next/static/chunks/0wuwlgcn6gxqt.js +0 -1
  195. package/out/_next/static/chunks/0xl5_avhu._i8.js +0 -1
  196. package/out/_next/static/chunks/10kvl8vj_plm-.js +0 -1
  197. package/out/_next/static/chunks/16m27azcs4k6w.js +0 -1
  198. package/out/changelog/__next._full.txt +0 -25
  199. package/out/changelog/__next._tree.txt +0 -5
  200. package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
  201. package/out/changelog/__next.changelog.txt +0 -5
  202. package/out/changelog/index.html +0 -15
  203. package/out/changelog/index.txt +0 -25
  204. package/out/docs/__next._full.txt +0 -25
  205. package/out/docs/__next._head.txt +0 -5
  206. package/out/docs/__next._tree.txt +0 -5
  207. package/out/docs/__next.docs.__PAGE__.txt +0 -10
  208. package/out/docs/__next.docs.txt +0 -5
  209. package/out/docs/getting-started/__next._full.txt +0 -25
  210. package/out/docs/getting-started/__next._head.txt +0 -5
  211. package/out/docs/getting-started/__next._index.txt +0 -9
  212. package/out/docs/getting-started/__next._tree.txt +0 -5
  213. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
  214. package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
  215. package/out/docs/getting-started/__next.docs.txt +0 -5
  216. package/out/docs/getting-started/index.html +0 -15
  217. package/out/docs/getting-started/index.txt +0 -25
  218. package/out/docs/index.html +0 -15
  219. package/out/docs/index.txt +0 -25
  220. package/out/note/edit/__next._full.txt +0 -24
  221. package/out/note/edit/__next._head.txt +0 -5
  222. package/out/note/edit/__next._index.txt +0 -9
  223. package/out/note/edit/__next._tree.txt +0 -4
  224. package/out/note/edit/__next.note.edit.__PAGE__.txt +0 -9
  225. package/out/note/edit/__next.note.edit.txt +0 -5
  226. package/out/note/edit/__next.note.txt +0 -5
  227. package/out/note/edit/index.html +0 -15
  228. package/out/note/edit/index.txt +0 -24
  229. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_buildManifest.js +0 -0
  230. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_clientMiddlewareManifest.js +0 -0
  231. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_ssgManifest.js +0 -0
package/server/index.js CHANGED
@@ -1,1280 +1,48 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import os from 'node:os'
4
3
  import { fileURLToPath } from 'node:url'
5
- import { spawn } from 'node:child_process'
6
- import Busboy from 'busboy'
7
4
  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
5
  import { serve } from '@hono/node-server'
12
6
  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'
7
+ import { createGanDengYanSocketHandlers } from './src/games/gandengyan.js'
16
8
  import {
9
+ DEFAULT_NODE_HOST,
10
+ DEFAULT_NODE_PORT,
17
11
  createNodeConfigStore,
18
- evaluateStorageLimits,
19
12
  } from './src/node/config.js'
20
13
  import { createNodeLogger } from './src/node/logs.js'
14
+ import {
15
+ UPLOAD_TMP_DIR,
16
+ createApp,
17
+ getDataPath,
18
+ } from './src/http/app.js'
21
19
 
22
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
23
- const PORT = Number(process.env.MOSTBOX_PORT || process.env.PORT) || 1976
24
- const HOST = process.env.MOSTBOX_HOST || '0.0.0.0'
25
-
26
- const UPLOAD_TMP_DIR = path.join(os.tmpdir(), 'most-box-uploads')
27
-
28
- const RATE_LIMIT_WINDOW = 60 * 1000
29
- const RATE_LIMIT_MAX_REQUESTS = 120
30
-
31
- // --- 配置 ---
32
- const defaultConfigStore = createNodeConfigStore()
33
- const defaultNodeLogger = createNodeLogger(defaultConfigStore.configDir)
34
- const CONFIG_DIR = defaultConfigStore.configDir
35
- const PACKAGE_JSON = readPackageJson()
36
-
37
- function getApiErrorStatus(err) {
38
- switch (err.code) {
39
- case 'VALIDATION_ERROR':
40
- case 'PATH_SECURITY_ERROR':
41
- case 'FILE_SIZE_ERROR':
42
- return 400
43
- case 'PEER_NOT_FOUND':
44
- return 503
45
- case 'INTEGRITY_ERROR':
46
- return 422
47
- case 'CONFLICT':
48
- return 409
49
- case 'PERMISSION_ERROR':
50
- return 403
51
- case 'ENGINE_NOT_INITIALIZED':
52
- return 503
53
- default:
54
- return 500
55
- }
56
- }
57
-
58
- function errorJson(c, err) {
59
- return c.json(
60
- {
61
- error: err.message,
62
- code: err.code || 'UNKNOWN',
63
- },
64
- getApiErrorStatus(err)
65
- )
66
- }
67
-
68
- function readPackageJson() {
69
- try {
70
- return JSON.parse(
71
- fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')
72
- )
73
- } catch {
74
- return { version: '0.0.0' }
75
- }
76
- }
77
-
78
- function getDataPath(configStore = defaultConfigStore) {
79
- return configStore.getDataPath()
80
- }
81
-
82
- function resolveDataPathForSave(inputPath) {
83
- let dataPath = String(inputPath || '').trim()
84
- let basePath = dataPath
85
-
86
- if (!dataPath) {
87
- return { dataPath: '' }
88
- }
89
-
90
- if (dataPath.match(/^[A-Za-z]:\\$/)) {
91
- basePath = dataPath
92
- dataPath = path.join(dataPath, 'most-data')
93
- }
94
-
95
- if (!fs.existsSync(basePath)) {
96
- return { error: '目录不存在' }
97
- }
98
-
99
- if (!fs.existsSync(dataPath)) {
100
- fs.mkdirSync(dataPath, { recursive: true })
101
- }
102
-
103
- return { dataPath }
104
- }
105
-
106
- function getNetworkAddresses(appPort) {
107
- const interfaces = os.networkInterfaces()
108
- const addresses = []
109
- const seen = new Set()
110
-
111
- for (const [name, nets] of Object.entries(interfaces)) {
112
- for (const net of nets) {
113
- if (net.family !== 'IPv4' || net.internal) continue
114
- if (seen.has(net.address)) continue
115
- seen.add(net.address)
116
-
117
- let type = 'lan'
118
- let label = '局域网'
119
- if (net.address.startsWith('100.')) {
120
- type = 'tailscale'
121
- label = 'Tailscale'
122
- } else if (
123
- name.toLowerCase().includes('zt') ||
124
- name.toLowerCase().includes('zerotier')
125
- ) {
126
- type = 'zerotier'
127
- label = 'ZeroTier'
128
- }
129
-
130
- addresses.push({ type, ip: net.address, label, iface: name })
131
- }
132
- }
133
-
134
- const localEntry = {
135
- type: 'local',
136
- ip: 'localhost',
137
- label: '本机',
138
- iface: 'loopback',
139
- }
140
- return { port: appPort, addresses: [localEntry, ...addresses] }
141
- }
142
-
143
- async function buildNodeStatus(engine, configStore, appPort, host) {
144
- const config = configStore.getNodeConfig()
145
- const storage = await engine.getStorageStats()
146
- const network = engine.getNetworkStatus()
147
- const holdings = engine.listHoldings()
148
-
149
- return {
150
- status: 'online',
151
- version: PACKAGE_JSON.version,
152
- uptimeSeconds: Math.floor(process.uptime()),
153
- nodeId: engine.getNodeId(),
154
- host,
155
- port: appPort,
156
- listen: getNetworkAddresses(appPort),
157
- dataPath: getDataPath(configStore),
158
- config,
159
- policy: {
160
- maxFileSizeBytes: config.maxFileSizeBytes,
161
- },
162
- capacity: {
163
- configuredBytes: config.capacityBytes,
164
- usedBytes: storage.used,
165
- freeBytes: Math.max(0, config.capacityBytes - storage.used),
166
- },
167
- storage,
168
- network,
169
- holdings,
170
- }
171
- }
172
-
173
- function buildOpenApiSpec(appPort) {
174
- return {
175
- openapi: '3.1.0',
176
- info: {
177
- title: 'MostBox Node Daemon API',
178
- version: PACKAGE_JSON.version,
179
- },
180
- servers: [{ url: `http://localhost:${appPort}` }],
181
- paths: {
182
- '/api/node/status': {
183
- get: {
184
- summary: 'Get node daemon status',
185
- responses: { 200: { description: 'Node status' } },
186
- },
187
- },
188
- '/api/node/config': {
189
- get: {
190
- summary: 'Get node daemon config',
191
- responses: { 200: { description: 'Node config' } },
192
- },
193
- post: {
194
- summary: 'Update node daemon config',
195
- responses: { 200: { description: 'Updated config' } },
196
- },
197
- },
198
- '/api/node/policy': {
199
- get: {
200
- summary: 'Get local storage limits',
201
- responses: { 200: { description: 'Storage limits' } },
202
- },
203
- post: {
204
- summary: 'Update local storage limits',
205
- responses: { 200: { description: 'Updated storage limits' } },
206
- },
207
- },
208
- '/api/node/policy/evaluate': {
209
- post: {
210
- summary: 'Evaluate a local file against storage limits',
211
- responses: { 200: { description: 'Storage limit decision' } },
212
- },
213
- },
214
- '/api/node/holdings': {
215
- get: {
216
- summary: 'List CID replicas held by this node',
217
- responses: { 200: { description: 'Node holdings' } },
218
- },
219
- post: {
220
- summary: 'Add a held CID replica record and join its topic',
221
- responses: { 200: { description: 'Created holding' } },
222
- },
223
- },
224
- '/api/node/logs': {
225
- get: {
226
- summary: 'Read recent node daemon logs',
227
- responses: { 200: { description: 'Node logs' } },
228
- },
229
- delete: {
230
- summary: 'Clear node daemon logs',
231
- responses: { 200: { description: 'Logs cleared' } },
232
- },
233
- },
234
- '/api/storage': {
235
- get: {
236
- summary: 'Get storage statistics',
237
- responses: { 200: { description: 'Storage statistics' } },
238
- },
239
- },
240
- '/api/p2p/pull': {
241
- post: {
242
- summary: 'Pull a full file replica by CID',
243
- responses: { 200: { description: 'Pull task result' } },
244
- },
245
- },
246
- },
247
- }
248
- }
249
-
250
- // --- 静态文件服务 ---
251
- const MIME_TYPES = {
252
- '.html': 'text/html; charset=utf-8',
253
- '.js': 'application/javascript; charset=utf-8',
254
- '.css': 'text/css; charset=utf-8',
255
- '.json': 'application/json',
256
- '.png': 'image/png',
257
- '.jpg': 'image/jpeg',
258
- '.jpeg': 'image/jpeg',
259
- '.gif': 'image/gif',
260
- '.webp': 'image/webp',
261
- '.svg': 'image/svg+xml',
262
- '.ico': 'image/x-icon',
263
- '.mp4': 'video/mp4',
264
- '.webm': 'video/webm',
265
- '.ogg': 'video/ogg',
266
- '.mp3': 'audio/mpeg',
267
- '.wav': 'audio/wav',
268
- '.flac': 'audio/flac',
269
- '.aac': 'audio/aac',
270
- '.m4a': 'audio/mp4',
271
- '.opus': 'audio/opus',
272
- '.woff2': 'font/woff2',
273
- '.woff': 'font/woff',
274
- }
275
-
276
- function getMimeType(fileName) {
277
- const ext = path.extname(fileName).toLowerCase()
278
- return MIME_TYPES[ext] || 'application/octet-stream'
279
- }
280
-
281
- function decodeFilenameFromHeader(headerStr) {
282
- if (!headerStr) return null
283
-
284
- const filenameStarMatch = headerStr.match(
285
- /filename\*=(?:UTF-8''|utf-8'')([^;\r\n]+)/i
286
- )
287
- if (filenameStarMatch) {
288
- return decodeURIComponent(filenameStarMatch[1])
289
- }
290
-
291
- const filenameMatch = headerStr.match(/filename="([^"]+)"/)
292
- if (filenameMatch) {
293
- const rawFilename = filenameMatch[1]
294
- try {
295
- const buf = Buffer.from(rawFilename, 'latin1')
296
- const decoded = buf.toString('utf8')
297
- if (decoded.includes('\ufffd')) {
298
- return rawFilename
299
- }
300
- return decoded
301
- } catch {
302
- return rawFilename
303
- }
304
- }
305
-
306
- const filenamePlainMatch = headerStr.match(/filename=([^;\r\n]+)/)
307
- if (filenamePlainMatch) {
308
- return filenamePlainMatch[1].trim()
309
- }
310
- return null
311
- }
312
-
313
- async function parseMultipartBusboy(req, maxUploadSize = MAX_FILE_SIZE) {
314
- return new Promise((resolve, reject) => {
315
- if (!fs.existsSync(UPLOAD_TMP_DIR)) {
316
- fs.mkdirSync(UPLOAD_TMP_DIR, { recursive: true })
317
- }
318
-
319
- const busboy = Busboy({
320
- headers: req.headers,
321
- limits: {
322
- fileSize: maxUploadSize,
323
- files: 1,
324
- fields: 0,
325
- },
326
- })
327
-
328
- const result = { filePath: null, filename: null }
329
- let fileSize = 0
330
- let writeStream = null
331
- let tempPath = null
332
-
333
- busboy.on('file', (name, stream, info) => {
334
- result.filename = decodeFilenameFromHeader(`filename="${info.filename}"`)
335
- tempPath = path.join(
336
- UPLOAD_TMP_DIR,
337
- `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
338
- )
339
- writeStream = fs.createWriteStream(tempPath)
340
-
341
- stream.on('data', chunk => {
342
- fileSize += chunk.length
343
- if (fileSize > maxUploadSize) {
344
- stream.destroy()
345
- writeStream.destroy()
346
- fs.unlink(tempPath, () => {})
347
- reject(new Error('File too large'))
348
- return
349
- }
350
- })
351
-
352
- stream.on('error', () => {
353
- if (tempPath) fs.unlink(tempPath, () => {})
354
- })
355
-
356
- stream.pipe(writeStream)
357
-
358
- writeStream.on('finish', () => {
359
- result.filePath = tempPath
360
- resolve(result)
361
- })
362
-
363
- writeStream.on('error', err => {
364
- if (tempPath) fs.unlink(tempPath, () => {})
365
- reject(err)
366
- })
367
- })
368
-
369
- busboy.on('error', err => {
370
- if (tempPath) fs.unlink(tempPath, () => {})
371
- reject(err)
372
- })
373
-
374
- busboy.on('close', () => {
375
- if (!result.filename) {
376
- resolve(null)
377
- }
378
- })
379
-
380
- req.on('error', err => {
381
- if (tempPath) fs.unlink(tempPath, () => {})
382
- reject(err)
383
- })
384
- req.pipe(busboy)
385
- })
386
- }
387
-
388
- // --- Hono 应用工厂 ---
389
- export function createApp(engine, options = {}) {
390
- const appPort = options.port || PORT
391
- const appHost = options.host || HOST
392
- const configStore = options.configStore || defaultConfigStore
393
- const nodeLogger =
394
- options.nodeLogger || createNodeLogger(configStore.configDir || CONFIG_DIR)
395
- const wssRef = options.wssRef || { current: null }
396
- const serverInstanceRef = options.serverInstanceRef || { current: null }
397
-
398
- // 速率限制(每个 app 实例独立)
399
- const rateLimitMap = new Map()
400
- function checkRateLimit(clientIp) {
401
- const now = Date.now()
402
- if (!rateLimitMap.has(clientIp)) {
403
- rateLimitMap.set(clientIp, [])
404
- }
405
- const requests = rateLimitMap.get(clientIp)
406
- while (requests.length > 0 && requests[0] < now - RATE_LIMIT_WINDOW) {
407
- requests.shift()
408
- }
409
- if (requests.length === 0) {
410
- rateLimitMap.delete(clientIp)
411
- }
412
- if (requests.length >= RATE_LIMIT_MAX_REQUESTS) {
413
- return false
414
- }
415
- requests.push(now)
416
- return true
417
- }
418
-
419
- function rateLimitMiddleware() {
420
- return async (c, next) => {
421
- const clientIp =
422
- c.req.header('x-forwarded-for') ||
423
- c.env?.incoming?.socket?.remoteAddress ||
424
- 'unknown'
425
- if (!checkRateLimit(clientIp)) {
426
- return c.json({ error: 'Too many requests' }, 429)
427
- }
428
- await next()
429
- }
430
- }
431
-
432
- // WebSocket 广播
433
- const channelSubscriptions = new Map()
434
-
435
- function wsBroadcast(event, data) {
436
- const payload = JSON.stringify({ event, data })
437
- const wss = wssRef.current
438
- if (wss) {
439
- wss.clients.forEach(client => {
440
- if (client.readyState === 1) {
441
- try {
442
- client.send(payload)
443
- } catch (err) {
444
- console.warn('[WS] Failed to send to client:', err.message)
445
- }
446
- }
447
- })
448
- }
449
- }
450
-
451
- async function broadcastNodeStatus() {
452
- try {
453
- const status = await buildNodeStatus(
454
- engine,
455
- configStore,
456
- appPort,
457
- appHost
458
- )
459
- wsBroadcast('node:status', status)
460
- return status
461
- } catch (err) {
462
- const entry = nodeLogger.append({
463
- level: 'error',
464
- event: 'node:status:error',
465
- message: err.message,
466
- })
467
- wsBroadcast('node:log', entry)
468
- return null
469
- }
470
- }
471
-
472
- function appendNodeLog(input) {
473
- const entry = nodeLogger.append(input)
474
- wsBroadcast('node:log', entry)
475
- return entry
476
- }
477
-
478
- function wsSendToChannel(channelName, event, data) {
479
- const payload = JSON.stringify({ event, data })
480
- const subscribers = channelSubscriptions.get(channelName)
481
- if (subscribers) {
482
- subscribers.forEach(ws => {
483
- if (ws.readyState === 1) {
484
- try {
485
- ws.send(payload)
486
- } catch (err) {
487
- console.warn(
488
- '[WS] Failed to send to channel subscriber:',
489
- err.message
490
- )
491
- }
492
- }
493
- })
494
- }
495
- }
496
-
497
- function subscribeToChannel(ws, channelName) {
498
- if (!channelSubscriptions.has(channelName)) {
499
- channelSubscriptions.set(channelName, new Set())
500
- }
501
- channelSubscriptions.get(channelName).add(ws)
502
- }
503
-
504
- function unsubscribeFromChannel(ws, channelName) {
505
- const subscribers = channelSubscriptions.get(channelName)
506
- if (subscribers) {
507
- subscribers.delete(ws)
508
- if (subscribers.size === 0) {
509
- channelSubscriptions.delete(channelName)
510
- }
511
- }
512
- }
513
-
514
- function cleanupWsSubscriptions(ws) {
515
- for (const [channel, subscribers] of channelSubscriptions) {
516
- subscribers.delete(ws)
517
- if (subscribers.size === 0) {
518
- channelSubscriptions.delete(channel)
519
- }
520
- }
521
- }
522
-
523
- // 将广播函数挂载到 engine 上供外部测试使用
524
- engine.wsBroadcast = wsBroadcast
525
- engine.wsSendToChannel = wsSendToChannel
526
-
527
- const app = new Hono()
528
-
529
- // CORS 中间件
530
- app.use(
531
- '/api/*',
532
- cors({
533
- origin: [
534
- 'http://localhost:3000',
535
- 'http://127.0.0.1:3000',
536
- 'https://most.box',
537
- `http://localhost:${appPort}`,
538
- `http://127.0.0.1:${appPort}`,
539
- ],
540
- credentials: true,
541
- })
542
- )
543
-
544
- // 速率限制中间件
545
- app.use('/api/*', rateLimitMiddleware())
546
-
547
- // 全局错误处理
548
- app.onError((err, c) => {
549
- console.error('[API Error]', err)
550
- try {
551
- const errorLogDir = configStore.configDir || CONFIG_DIR
552
- const errorLogPath = path.join(errorLogDir, 'server-error.log')
553
- if (!fs.existsSync(errorLogDir)) {
554
- fs.mkdirSync(errorLogDir, { recursive: true })
555
- }
556
- fs.appendFileSync(
557
- errorLogPath,
558
- `[${new Date().toISOString()}] ${err.stack}\n`
559
- )
560
- } catch {}
561
- return c.json({ error: err.message, code: err.code }, 500)
562
- })
563
-
564
- // --- 配置路由 ---
565
- app.get('/api/node-id', c => {
566
- return c.json({ id: engine.getNodeId() })
567
- })
568
-
569
- app.get('/api/config', c => {
570
- const config = configStore.loadRawConfig()
571
- return c.json({ dataPath: config.dataPath || '' })
572
- })
573
-
574
- app.post('/api/config', async c => {
575
- const body = await c.req.json()
576
- const patch = {}
577
-
578
- if (body.resetStorage) {
579
- patch.dataPath = ''
580
- } else if (body.dataPath !== undefined) {
581
- const resolved = resolveDataPathForSave(body.dataPath)
582
- if (resolved.error) return c.json({ error: resolved.error }, 400)
583
- patch.dataPath = resolved.dataPath
584
- }
585
-
586
- const { success } = configStore.saveNodeConfigPatch(patch)
587
- appendNodeLog({
588
- event: 'node:config:updated',
589
- message: 'Node config updated',
590
- data: { dataPath: getDataPath(configStore) },
591
- })
592
- await broadcastNodeStatus()
593
- return c.json({ success, dataPath: getDataPath(configStore) })
594
- })
595
-
596
- app.get('/api/config/data-path', c => {
597
- const config = configStore.getNodeConfig()
598
- const isDefault = !config.dataPath
599
- const dataPath = getDataPath(configStore)
600
- return c.json({ dataPath, isDefault })
601
- })
602
-
603
- app.get('/api/node/status', async c => {
604
- try {
605
- return c.json(
606
- await buildNodeStatus(engine, configStore, appPort, appHost)
607
- )
608
- } catch (err) {
609
- return errorJson(c, err)
610
- }
611
- })
612
-
613
- app.get('/api/node/config', c => {
614
- const config = configStore.getNodeConfig()
615
- return c.json({
616
- ...config,
617
- dataPath: getDataPath(configStore),
618
- configuredDataPath: config.dataPath,
619
- isDefaultDataPath: !config.dataPath && !process.env.MOSTBOX_DATA_PATH,
620
- envDataPath: process.env.MOSTBOX_DATA_PATH || null,
621
- })
622
- })
623
-
624
- app.post('/api/node/config', async c => {
625
- const body = await c.req.json()
626
- const patch = { ...body }
627
-
628
- if (body.resetStorage) {
629
- patch.dataPath = ''
630
- } else if (body.dataPath !== undefined) {
631
- const resolved = resolveDataPathForSave(body.dataPath)
632
- if (resolved.error) return c.json({ error: resolved.error }, 400)
633
- patch.dataPath = resolved.dataPath
634
- }
635
-
636
- const { success, config } = configStore.saveNodeConfigPatch(patch)
637
- engine.setMaxFileSize(config.maxFileSizeBytes)
638
- appendNodeLog({
639
- event: 'node:config:updated',
640
- message: 'Node daemon config updated',
641
- data: {
642
- dataPath: getDataPath(configStore),
643
- capacityBytes: config.capacityBytes,
644
- },
645
- })
646
- await broadcastNodeStatus()
647
- return c.json({ success, ...config, dataPath: getDataPath(configStore) })
648
- })
649
-
650
- app.get('/api/node/policy', c => {
651
- const config = configStore.getNodeConfig()
652
- return c.json({
653
- maxFileSizeBytes: config.maxFileSizeBytes,
654
- })
655
- })
656
-
657
- app.post('/api/node/policy', async c => {
658
- const body = await c.req.json()
659
- const { success, config } = configStore.saveNodeConfigPatch({
660
- maxFileSizeBytes: body.maxFileSizeBytes,
661
- })
662
- engine.setMaxFileSize(config.maxFileSizeBytes)
663
- const policy = {
664
- maxFileSizeBytes: config.maxFileSizeBytes,
665
- }
666
- appendNodeLog({
667
- event: 'node:policy:updated',
668
- message: 'Node storage limits updated',
669
- data: policy,
670
- })
671
- await broadcastNodeStatus()
672
- return c.json({ success, ...policy })
673
- })
674
-
675
- app.post('/api/node/policy/evaluate', async c => {
676
- const body = await c.req.json()
677
- const decision = evaluateStorageLimits(configStore.getNodeConfig(), body)
678
- return c.json(decision)
679
- })
680
-
681
- app.get('/api/node/logs', c => {
682
- const limit = Number(c.req.query('limit') || 100)
683
- return c.json({
684
- logFile: nodeLogger.logFile,
685
- logs: nodeLogger.list(limit),
686
- })
687
- })
688
-
689
- app.delete('/api/node/logs', c => {
690
- const success = nodeLogger.clear()
691
- const clearedAt = new Date().toISOString()
692
- wsBroadcast('node:logs:cleared', { clearedAt })
693
- return c.json({ success, clearedAt })
694
- })
695
-
696
- app.get('/api/openapi.json', c => {
697
- return c.json(buildOpenApiSpec(appPort))
698
- })
699
-
700
- // --- 网络路由 ---
701
- app.get('/api/network-status', c => {
702
- return c.json(engine.getNetworkStatus())
703
- })
704
-
705
- app.get('/api/network', c => {
706
- return c.json(getNetworkAddresses(appPort))
707
- })
708
-
709
- // --- 节点保种路由 ---
710
- app.get('/api/node/holdings', c => {
711
- try {
712
- return c.json(engine.listHoldings())
713
- } catch (err) {
714
- return errorJson(c, err)
715
- }
716
- })
717
-
718
- app.post('/api/node/holdings', async c => {
719
- try {
720
- const body = await c.req.json()
721
- const holding = await engine.addHolding(body)
722
- appendNodeLog({
723
- event: 'node:holding:added',
724
- message: 'Node holding added',
725
- data: { cid: holding.cid, size: holding.size },
726
- })
727
- await broadcastNodeStatus()
728
- return c.json({ success: true, holding })
729
- } catch (err) {
730
- return errorJson(c, err)
731
- }
732
- })
733
-
734
- app.post('/api/p2p/pull', async c => {
735
- try {
736
- const body = await c.req.json()
737
- const timeout =
738
- body.timeout === undefined ? undefined : Number(body.timeout)
739
- const result = await engine.pullByCid({
740
- ...body,
741
- timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
742
- })
743
- appendNodeLog({
744
- event: 'node:pull:success',
745
- message: 'P2P pull completed',
746
- data: { cid: result.cid, taskId: result.taskId },
747
- })
748
- await broadcastNodeStatus()
749
- return c.json({ success: true, ...result })
750
- } catch (err) {
751
- appendNodeLog({
752
- level: 'error',
753
- event: 'node:pull:error',
754
- message: err.message,
755
- data: { code: err.code || 'UNKNOWN' },
756
- })
757
- return errorJson(c, err)
758
- }
759
- })
760
-
761
- // --- 文件路由 ---
762
- app.get('/api/files', c => {
763
- return c.json(engine.listPublishedFiles())
764
- })
765
-
766
- app.post('/api/publish', async c => {
767
- const req = c.env.incoming
768
- const result = await parseMultipartBusboy(
769
- req,
770
- configStore.getNodeConfig().maxFileSizeBytes
771
- )
772
-
773
- if (!result || !result.filename) {
774
- return c.json({ error: 'No file provided' }, 400)
775
- }
776
-
777
- try {
778
- const publishResult = await engine.publishFile(
779
- result.filePath,
780
- result.filename,
781
- { localPath: null }
782
- )
783
- return c.json({ success: true, ...publishResult })
784
- } finally {
785
- fs.unlink(result.filePath, () => {})
786
- }
787
- })
788
-
789
- app.post('/api/download/check', async c => {
790
- const body = await c.req.json()
791
- if (!body.link) {
792
- return c.json({ error: 'link is required' }, 400)
793
- }
794
-
795
- const parsed = parseMostLink(body.link)
796
- if (parsed.error) {
797
- return c.json({ error: parsed.error }, 400)
798
- }
799
-
800
- const existingFile = engine
801
- .getPublishedFiles()
802
- .find(f => f.cid === parsed.cid)
803
- if (existingFile) {
804
- return c.json({
805
- success: true,
806
- available: true,
807
- cid: parsed.cid,
808
- fileName: existingFile.fileName,
809
- size: Number(existingFile.size) || null,
810
- alreadyExists: true,
811
- })
812
- }
813
-
814
- if (engine.hasDownloadNameConflict(parsed.fileName)) {
815
- return c.json(
816
- {
817
- error: `已有同名文件: ${parsed.fileName}`,
818
- code: 'CONFLICT',
819
- },
820
- 409
821
- )
822
- }
823
-
824
- try {
825
- const result = await engine.checkDownloadAvailability(body.link)
826
- return c.json({ success: true, ...result })
827
- } catch (err) {
828
- return errorJson(c, err)
829
- }
830
- })
831
-
832
- app.post('/api/download', async c => {
833
- const body = await c.req.json()
834
- if (!body.link) {
835
- return c.json({ error: 'link is required' }, 400)
836
- }
837
-
838
- const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
839
-
840
- const parsed = parseMostLink(body.link)
841
- if (parsed.error) {
842
- return c.json({ error: parsed.error }, 400)
843
- }
844
-
845
- const existingFile = engine
846
- .getPublishedFiles()
847
- .find(f => f.cid === parsed.cid)
848
- if (existingFile) {
849
- console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
850
- try {
851
- const result = await engine.downloadFile(body.link, taskId)
852
- return c.json({ success: true, ...result })
853
- } catch (err) {
854
- return errorJson(c, err)
855
- }
856
- }
857
-
858
- if (engine.hasDownloadNameConflict(parsed.fileName)) {
859
- return c.json(
860
- {
861
- error: `已有同名文件: ${parsed.fileName}`,
862
- code: 'CONFLICT',
863
- },
864
- 409
865
- )
866
- }
867
-
868
- engine.downloadFile(body.link, taskId).catch(err => {
869
- if (err.message === 'Download cancelled') {
870
- wsBroadcast('download:cancelled', { taskId })
871
- } else {
872
- wsBroadcast('download:error', { taskId, error: err.message })
873
- }
874
- })
875
-
876
- return c.json({ success: true, taskId })
877
- })
878
-
879
- app.post('/api/download/cancel', async c => {
880
- const body = await c.req.json()
881
- if (!body.taskId) {
882
- return c.json({ error: 'taskId is required' }, 400)
883
- }
884
- engine.cancelDownload(body.taskId)
885
- return c.json({ success: true })
886
- })
887
-
888
- app.delete('/api/files/:cid', async c => {
889
- const cid = c.req.param('cid')
890
- const cidValidation = validateCidString(cid)
891
- if (!cidValidation.valid) {
892
- return c.json({ error: cidValidation.error }, 400)
893
- }
894
- const result = await engine.deletePublishedFile(cid)
895
- return c.json(result)
896
- })
897
-
898
- app.post('/api/move', async c => {
899
- const body = await c.req.json()
900
- if (!body.cid || !body.newFileName) {
901
- return c.json({ error: 'cid and newFileName are required' }, 400)
902
- }
903
- const cidValidation = validateCidString(body.cid)
904
- if (!cidValidation.valid) {
905
- return c.json({ error: cidValidation.error }, 400)
906
- }
907
- const cleanFileName = sanitizeFilename(body.newFileName)
908
- if (
909
- !cleanFileName ||
910
- cleanFileName === 'unnamed' ||
911
- body.newFileName.length > 255
912
- ) {
913
- return c.json({ error: 'Invalid filename' }, 400)
914
- }
915
- try {
916
- const result = engine.moveFile(body.cid, cleanFileName)
917
- return c.json({ success: true, ...result })
918
- } catch (err) {
919
- return c.json({ error: err.message }, 400)
920
- }
921
- })
922
-
923
- app.get('/api/files/:cid/download', async c => {
924
- const cid = c.req.param('cid')
925
- const cidValidation = validateCidString(cid)
926
- if (!cidValidation.valid) {
927
- return c.json({ error: cidValidation.error }, 400)
928
- }
929
-
930
- const rangeHeader = c.req.header('range')
931
-
932
- try {
933
- if (rangeHeader) {
934
- const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
935
- if (rangeMatch) {
936
- const start = parseInt(rangeMatch[1], 10)
937
- const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
938
- const offset = start
939
- const limit = end !== undefined ? end - start + 1 : undefined
940
-
941
- const result = await engine.readFileRaw(cid, { offset, limit })
942
- const contentType = getMimeType(result.fileName)
943
-
944
- c.header('Content-Type', contentType)
945
- c.header('Content-Length', String(result.buffer.length))
946
- c.header(
947
- 'Content-Range',
948
- `bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`
949
- )
950
- c.header('Accept-Ranges', 'bytes')
951
- c.status(206)
952
- return c.body(result.buffer)
953
- }
954
- }
955
-
956
- const result = await engine.readFileRaw(cid)
957
- const contentType = getMimeType(result.fileName)
958
- c.header('Content-Type', contentType)
959
- c.header('Content-Length', String(result.totalSize))
960
- c.header('Accept-Ranges', 'bytes')
961
- c.header(
962
- 'Content-Disposition',
963
- `inline; filename="${encodeURIComponent(result.fileName)}"`
964
- )
965
- return c.body(result.buffer)
966
- } catch (err) {
967
- if (err.message === 'File not found') {
968
- return c.json({ error: err.message }, 404)
969
- }
970
- return c.json({ error: err.message }, 400)
971
- }
972
- })
973
-
974
- // --- 回收站路由 ---
975
- app.get('/api/trash', c => {
976
- return c.json(engine.listTrashFiles())
977
- })
978
-
979
- app.post('/api/trash/:cid/restore', async c => {
980
- const cid = c.req.param('cid')
981
- const cidValidation = validateCidString(cid)
982
- if (!cidValidation.valid) {
983
- return c.json({ error: cidValidation.error }, 400)
984
- }
985
- try {
986
- const result = await engine.restoreTrashFile(cid)
987
- return c.json({ success: true, files: result })
988
- } catch (err) {
989
- return c.json({ error: err.message }, 400)
990
- }
991
- })
992
-
993
- app.delete('/api/trash/:cid', async c => {
994
- const cid = c.req.param('cid')
995
- const cidValidation = validateCidString(cid)
996
- if (!cidValidation.valid) {
997
- return c.json({ error: cidValidation.error }, 400)
998
- }
999
- const result = await engine.permanentDeleteTrashFile(cid)
1000
- return c.json({ success: true, trashFiles: result })
1001
- })
1002
-
1003
- app.delete('/api/trash', async c => {
1004
- const result = await engine.emptyTrash()
1005
- return c.json({ success: true, trashFiles: result })
1006
- })
1007
-
1008
- // --- 收藏路由 ---
1009
- app.post('/api/files/:cid/star', async c => {
1010
- const cid = c.req.param('cid')
1011
- const cidValidation = validateCidString(cid)
1012
- if (!cidValidation.valid) {
1013
- return c.json({ error: cidValidation.error }, 400)
1014
- }
1015
- try {
1016
- const result = engine.toggleStarred(cid)
1017
- return c.json({ success: true, ...result })
1018
- } catch (err) {
1019
- return c.json({ error: err.message }, 400)
1020
- }
1021
- })
1022
-
1023
- // --- 存储路由 ---
1024
- app.get('/api/storage', async c => {
1025
- const result = await engine.getStorageStats()
1026
- return c.json(result)
1027
- })
20
+ export { createApp } from './src/http/app.js'
1028
21
 
1029
- // --- 显示名路由 ---
1030
- app.get('/api/display-name', c => {
1031
- return c.json({ displayName: engine.getDisplayName() })
1032
- })
22
+ const PORT = DEFAULT_NODE_PORT
23
+ const HOST = DEFAULT_NODE_HOST
1033
24
 
1034
- app.post('/api/display-name', async c => {
1035
- const body = await c.req.json()
1036
- if (!body.name || !body.name.trim()) {
1037
- return c.json({ error: 'name is required' }, 400)
1038
- }
1039
- const trimmed = body.name.trim()
1040
- if (trimmed.length > 100) {
1041
- return c.json({ error: 'Name too long (max 100 chars)' }, 400)
1042
- }
1043
- if (/[<>]/.test(trimmed)) {
1044
- return c.json({ error: 'Name contains invalid characters' }, 400)
1045
- }
1046
- const success = engine.setDisplayName(trimmed)
1047
- return c.json({ success, displayName: engine.getDisplayName() })
1048
- })
25
+ function cleanUploadTempDir() {
26
+ if (!fs.existsSync(UPLOAD_TMP_DIR)) return
1049
27
 
1050
- // --- 频道路由 ---
1051
- app.post('/api/channels', async c => {
1052
- const body = await c.req.json()
1053
- if (!body.name || !body.name.trim()) {
1054
- return c.json({ error: 'name is required' }, 400)
1055
- }
28
+ const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
29
+ for (const file of staleFiles) {
1056
30
  try {
1057
- const result = await engine.createChannel(
1058
- body.name.trim(),
1059
- body.type || 'personal'
1060
- )
1061
- return c.json({ success: true, ...result })
31
+ fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file))
1062
32
  } catch (err) {
1063
- return c.json({ error: err.message }, 400)
1064
- }
1065
- })
1066
-
1067
- app.get('/api/channels', c => {
1068
- return c.json(engine.listChannels())
1069
- })
1070
-
1071
- app.delete('/api/channels/:name', async c => {
1072
- const name = c.req.param('name')
1073
- try {
1074
- const result = await engine.leaveChannel(name)
1075
- return c.json({ success: true, channels: result })
1076
- } catch (err) {
1077
- return c.json({ error: err.message }, 400)
1078
- }
1079
- })
1080
-
1081
- app.get('/api/channels/:name/messages', async c => {
1082
- const name = c.req.param('name')
1083
- const limit = parseInt(c.req.query('limit') || '100', 10)
1084
- const offset = parseInt(c.req.query('offset') || '0', 10)
1085
- try {
1086
- const messages = await engine.getChannelMessages(name, { limit, offset })
1087
- return c.json(messages)
1088
- } catch (err) {
1089
- return c.json({ error: err.message }, 400)
1090
- }
1091
- })
1092
-
1093
- app.post('/api/channels/:name/messages', async c => {
1094
- const name = c.req.param('name')
1095
- const body = await c.req.json()
1096
- if (!body.content || !body.content.trim()) {
1097
- return c.json({ error: 'content is required' }, 400)
1098
- }
1099
- if (!body.author || !body.authorName) {
1100
- return c.json({ error: 'author and authorName are required' }, 400)
1101
- }
1102
- if (!/^0x[a-fA-F0-9]{40}$/.test(body.author)) {
1103
- return c.json({ error: 'Invalid author format' }, 400)
1104
- }
1105
- if (body.authorName.length > 50) {
1106
- return c.json({ error: 'authorName too long' }, 400)
1107
- }
1108
- try {
1109
- const message = await engine.sendMessage(
1110
- name,
1111
- body.content,
1112
- body.author,
1113
- body.authorName
1114
- )
1115
- return c.json({ success: true, message })
1116
- } catch (err) {
1117
- return c.json({ error: err.message }, 400)
1118
- }
1119
- })
1120
-
1121
- app.get('/api/channels/:name/peers', c => {
1122
- return c.json(engine.getChannelPeers(c.req.param('name')))
1123
- })
1124
-
1125
- // --- 文件夹重命名 ---
1126
- app.post('/api/folder/rename', async c => {
1127
- const body = await c.req.json()
1128
- if (!body.oldPath || !body.newPath) {
1129
- return c.json({ error: 'oldPath and newPath are required' }, 400)
1130
- }
1131
- if (body.oldPath.length > 500 || body.newPath.length > 500) {
1132
- return c.json({ error: 'Path too long' }, 400)
1133
- }
1134
- if (body.oldPath.includes('..') || body.newPath.includes('..')) {
1135
- return c.json({ error: 'Path traversal not allowed' }, 400)
1136
- }
1137
- try {
1138
- const result = engine.renameFolder(body.oldPath, body.newPath)
1139
- return c.json({ success: true, ...result })
1140
- } catch (err) {
1141
- return c.json({ error: err.message }, 400)
1142
- }
1143
- })
1144
-
1145
- // --- 关机路由 ---
1146
- app.post('/api/shutdown', c => {
1147
- const clientIp = c.env.incoming?.socket?.remoteAddress || 'unknown'
1148
- const isLocalhost =
1149
- clientIp === 'localhost' ||
1150
- clientIp === '::1' ||
1151
- clientIp === '::ffff:localhost' ||
1152
- clientIp === '127.0.0.1' ||
1153
- clientIp === '::ffff:127.0.0.1' ||
1154
- clientIp.startsWith('::ffff:127.')
1155
- if (!isLocalhost) {
1156
- return c.json({ error: 'Forbidden' }, 403)
1157
- }
1158
- c.json({ success: true })
1159
- console.log('[MostBox] Shutdown requested via API...')
1160
- setTimeout(async () => {
1161
- await engine.stop()
1162
- if (serverInstanceRef.current) serverInstanceRef.current.close()
1163
- console.log('[MostBox] Server stopped.')
1164
- process.exit(0)
1165
- }, 100)
1166
- return c.body(null)
1167
- })
1168
-
1169
- // --- 静态文件服务(SPA fallback) ---
1170
- const publicDir = path.join(__dirname, '..', 'out')
1171
-
1172
- app.get('/static/*', serveStatic({ root: './out' }))
1173
- app.get('/_next/*', serveStatic({ root: './out' }))
1174
-
1175
- app.all('/api/*', c => {
1176
- return c.json({ error: 'Not found' }, 404)
1177
- })
1178
-
1179
- app.get('*', async c => {
1180
- const pathname = c.req.path
1181
- const filePath = path.join(publicDir, pathname)
1182
- const resolved = path.resolve(filePath)
1183
- const resolvedPublic = path.resolve(publicDir)
1184
-
1185
- if (
1186
- !resolved.startsWith(resolvedPublic + path.sep) &&
1187
- resolved !== resolvedPublic
1188
- ) {
1189
- return c.json({ error: 'Not found' }, 404)
1190
- }
1191
-
1192
- if (fs.existsSync(filePath)) {
1193
- const stat = fs.statSync(filePath)
1194
- if (stat.isFile()) {
1195
- const ext = path.extname(filePath)
1196
- c.header('Content-Type', MIME_TYPES[ext] || 'application/octet-stream')
1197
- return c.body(fs.readFileSync(filePath))
1198
- }
1199
- if (stat.isDirectory()) {
1200
- const dirIndex = path.join(filePath, 'index.html')
1201
- if (fs.existsSync(dirIndex)) {
1202
- c.header('Content-Type', 'text/html; charset=utf-8')
1203
- return c.body(fs.readFileSync(dirIndex, 'utf-8'))
1204
- }
1205
- }
33
+ console.warn('[MostBox] Failed to clean upload temp file:', err.message)
1206
34
  }
1207
-
1208
- const indexPath = path.join(publicDir, 'index.html')
1209
- if (fs.existsSync(indexPath)) {
1210
- c.header('Content-Type', 'text/html; charset=utf-8')
1211
- return c.body(fs.readFileSync(indexPath, 'utf-8'))
1212
- }
1213
-
1214
- return c.json({ error: 'Not found' }, 404)
1215
- })
1216
-
1217
- return {
1218
- app,
1219
- wsBroadcast,
1220
- wsSendToChannel,
1221
- broadcastNodeStatus,
1222
- appendNodeLog,
1223
- subscribeToChannel,
1224
- unsubscribeFromChannel,
1225
- cleanupWsSubscriptions,
1226
35
  }
36
+ console.log(`[MostBox] Cleaned ${staleFiles.length} stale upload temp files`)
1227
37
  }
1228
38
 
1229
- // --- 主函数 ---
1230
- export async function main() {
1231
- console.log('[MostBox] Starting core daemon...')
1232
-
1233
- if (fs.existsSync(UPLOAD_TMP_DIR)) {
1234
- const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
1235
- for (const file of staleFiles) {
1236
- try {
1237
- fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file))
1238
- } catch (err) {
1239
- console.warn('[MostBox] Failed to clean upload temp file:', err.message)
1240
- }
1241
- }
1242
- console.log(
1243
- `[MostBox] Cleaned ${staleFiles.length} stale upload temp files`
1244
- )
1245
- }
1246
-
1247
- const configStore = defaultConfigStore
1248
- const nodeLogger = defaultNodeLogger
1249
- const dataPath = getDataPath(configStore)
1250
- console.log(`[MostBox] Storage: ${dataPath}`)
1251
-
1252
- const engine = new MostBoxEngine({
1253
- dataPath,
1254
- maxFileSize: configStore.getNodeConfig().maxFileSizeBytes,
1255
- })
1256
-
1257
- const wssRef = { current: null }
1258
- const serverInstanceRef = { current: null }
1259
-
1260
- const {
1261
- app,
1262
- wsBroadcast,
1263
- wsSendToChannel,
1264
- broadcastNodeStatus,
1265
- appendNodeLog,
1266
- subscribeToChannel,
1267
- unsubscribeFromChannel,
1268
- cleanupWsSubscriptions,
1269
- } = createApp(engine, {
1270
- port: PORT,
1271
- host: HOST,
1272
- configStore,
1273
- nodeLogger,
1274
- wssRef,
1275
- serverInstanceRef,
1276
- })
1277
-
39
+ function bindEngineEvents({
40
+ engine,
41
+ wsBroadcast,
42
+ wsSendToChannel,
43
+ appendNodeLog,
44
+ broadcastNodeStatus,
45
+ }) {
1278
46
  let engineReadyForStatus = false
1279
47
  const safeBroadcastNodeStatus = () => {
1280
48
  if (engineReadyForStatus) {
@@ -1334,6 +102,10 @@ export async function main() {
1334
102
  })
1335
103
  safeBroadcastNodeStatus()
1336
104
  })
105
+ engine.on('seed:metrics', data => {
106
+ wsBroadcast('seed:metrics', data)
107
+ safeBroadcastNodeStatus()
108
+ })
1337
109
  engine.on('channel:message', data =>
1338
110
  wsSendToChannel(data.channel, 'channel:message', data)
1339
111
  )
@@ -1346,43 +118,28 @@ export async function main() {
1346
118
  engine.on('channel:joined', data => wsBroadcast('channel:joined', data))
1347
119
  engine.on('channel:left', data => wsBroadcast('channel:left', data))
1348
120
 
1349
- await engine.start()
1350
- engineReadyForStatus = true
1351
- console.log('[MostBox] Engine ready')
1352
- appendNodeLog({
1353
- event: 'node:ready',
1354
- message: 'Node daemon ready',
1355
- data: { dataPath, port: PORT },
1356
- })
1357
- broadcastNodeStatus()
1358
-
1359
- serverInstanceRef.current = serve(
1360
- { fetch: app.fetch, port: PORT, hostname: HOST },
1361
- () => {
1362
- const displayUrl = `http://localhost:${PORT}`
1363
- console.log(`[MostBox] Server running at ${displayUrl}`)
1364
-
1365
- if (process.env.ELECTRON_APP) return
1366
-
1367
- if (process.platform === 'win32') {
1368
- spawn('cmd.exe', ['/c', 'start', '', displayUrl], {
1369
- detached: true,
1370
- stdio: 'ignore',
1371
- }).unref()
1372
- } else {
1373
- const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
1374
- spawn(cmd, [displayUrl], {
1375
- detached: true,
1376
- stdio: 'ignore',
1377
- }).unref()
1378
- }
1379
- }
1380
- )
121
+ return {
122
+ markReady() {
123
+ engineReadyForStatus = true
124
+ },
125
+ }
126
+ }
1381
127
 
1382
- wssRef.current = new WebSocketServer({ noServer: true })
1383
- wssRef.current.on('connection', ws => {
128
+ function createWebSocketServer({
129
+ serverInstance,
130
+ validateWebSocketRequest,
131
+ subscribeToChannel,
132
+ unsubscribeFromChannel,
133
+ cleanupWsSubscriptions,
134
+ }) {
135
+ const wss = new WebSocketServer({ noServer: true })
136
+ const ganDengYan = createGanDengYanSocketHandlers()
137
+
138
+ wss.on('connection', ws => {
139
+ ganDengYan.bindClient(ws)
1384
140
  ws.on('error', () => {})
1385
141
  ws.on('close', () => {
142
+ ganDengYan.unbindClient(ws)
1386
143
  cleanupWsSubscriptions(ws)
1387
144
  })
1388
145
  ws.on('message', raw => {
@@ -1390,6 +147,10 @@ export async function main() {
1390
147
  const msg = JSON.parse(raw)
1391
148
  const { event, data } = msg
1392
149
 
150
+ if (ganDengYan.handleMessage(ws, event, data)) {
151
+ return
152
+ }
153
+
1393
154
  switch (event) {
1394
155
  case 'register':
1395
156
  ws.peerId = data.peerId
@@ -1411,30 +172,107 @@ export async function main() {
1411
172
  })
1412
173
  })
1413
174
 
1414
- serverInstanceRef.current.on('upgrade', (req, socket, head) => {
1415
- if (req.url.startsWith('/ws')) {
1416
- wssRef.current.handleUpgrade(req, socket, head, ws => {
1417
- wssRef.current.emit('connection', ws, req)
1418
- })
1419
- } else {
175
+ serverInstance.on('upgrade', (req, socket, head) => {
176
+ if (!req.url.startsWith('/ws')) {
177
+ socket.destroy()
178
+ return
179
+ }
180
+
181
+ if (!validateWebSocketRequest(req)) {
1420
182
  socket.destroy()
183
+ return
1421
184
  }
185
+
186
+ wss.handleUpgrade(req, socket, head, ws => {
187
+ wss.emit('connection', ws, req)
188
+ })
1422
189
  })
1423
190
 
1424
- process.on('SIGINT', async () => {
1425
- console.log('\n[MostBox] Shutting down...')
191
+ return wss
192
+ }
193
+
194
+ function bindShutdownSignals({ engine, wssRef, serverInstanceRef }) {
195
+ async function shutdown(message) {
196
+ if (message) console.log(message)
1426
197
  await engine.stop()
1427
198
  if (wssRef.current) wssRef.current.close()
1428
199
  serverInstanceRef.current.close()
1429
200
  process.exit(0)
201
+ }
202
+
203
+ process.on('SIGINT', () => {
204
+ shutdown('\n[MostBox] Shutting down...')
1430
205
  })
1431
206
 
1432
- process.on('SIGTERM', async () => {
1433
- await engine.stop()
1434
- if (wssRef.current) wssRef.current.close()
1435
- serverInstanceRef.current.close()
1436
- process.exit(0)
207
+ process.on('SIGTERM', () => {
208
+ shutdown()
1437
209
  })
210
+ }
211
+
212
+ // --- 主函数 ---
213
+ export async function main() {
214
+ console.log('[MostBox] Starting core daemon...')
215
+ cleanUploadTempDir()
216
+
217
+ const configStore = createNodeConfigStore()
218
+ const nodeLogger = createNodeLogger(configStore.configDir)
219
+ const dataPath = getDataPath(configStore)
220
+ console.log(`[MostBox] Storage: ${dataPath}`)
221
+
222
+ const nodeConfig = configStore.getNodeConfig()
223
+ const engine = new MostBoxEngine({
224
+ dataPath,
225
+ maxFileSize: nodeConfig.maxFileSizeBytes,
226
+ capacityBytes: nodeConfig.capacityBytes,
227
+ })
228
+
229
+ const wssRef = { current: null }
230
+ const serverInstanceRef = { current: null }
231
+
232
+ const appRuntime = createApp(engine, {
233
+ port: PORT,
234
+ host: HOST,
235
+ configStore,
236
+ nodeLogger,
237
+ wssRef,
238
+ serverInstanceRef,
239
+ })
240
+
241
+ const engineEvents = bindEngineEvents({
242
+ engine,
243
+ wsBroadcast: appRuntime.wsBroadcast,
244
+ wsSendToChannel: appRuntime.wsSendToChannel,
245
+ appendNodeLog: appRuntime.appendNodeLog,
246
+ broadcastNodeStatus: appRuntime.broadcastNodeStatus,
247
+ })
248
+
249
+ await engine.start()
250
+ engineEvents.markReady()
251
+ console.log('[MostBox] Engine ready')
252
+ appRuntime.appendNodeLog({
253
+ event: 'node:ready',
254
+ message: 'Node daemon ready',
255
+ data: { dataPath, port: PORT },
256
+ })
257
+ appRuntime.broadcastNodeStatus()
258
+
259
+ serverInstanceRef.current = serve(
260
+ { fetch: appRuntime.app.fetch, port: PORT, hostname: HOST },
261
+ () => {
262
+ const displayUrl = `http://localhost:${PORT}`
263
+ console.log(`[MostBox] Server running at ${displayUrl}`)
264
+ }
265
+ )
266
+
267
+ wssRef.current = createWebSocketServer({
268
+ serverInstance: serverInstanceRef.current,
269
+ validateWebSocketRequest: appRuntime.validateWebSocketRequest,
270
+ subscribeToChannel: appRuntime.subscribeToChannel,
271
+ unsubscribeFromChannel: appRuntime.unsubscribeFromChannel,
272
+ cleanupWsSubscriptions: appRuntime.cleanupWsSubscriptions,
273
+ })
274
+
275
+ bindShutdownSignals({ engine, wssRef, serverInstanceRef })
1438
276
 
1439
277
  return engine
1440
278
  }