most-box 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +21 -14
  2. package/electron/main.js +72 -8
  3. package/electron/preload.js +6 -0
  4. package/out/404/index.html +2 -2
  5. package/out/404.html +2 -2
  6. package/out/__next.__PAGE__.txt +6 -6
  7. package/out/__next._full.txt +16 -16
  8. package/out/__next._head.txt +3 -3
  9. package/out/__next._index.txt +8 -8
  10. package/out/__next._tree.txt +5 -5
  11. package/out/_next/static/chunks/0.e2avjgna_b2.js +1 -0
  12. package/out/_next/static/chunks/{0ho~log~~-jwp.css → 03h~nhgj0hv3p.css} +1 -1
  13. package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
  14. package/out/_next/static/chunks/{0qou.u2e2dy48.css → 0adx~d-j05c9d.css} +2 -2
  15. package/out/_next/static/chunks/0aq.rc9woa2nz.js +1 -0
  16. package/out/_next/static/chunks/0etes81d_cihn.js +1 -0
  17. package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
  18. package/out/_next/static/chunks/{0o6lrkxy4jwag.js → 0gcsdf57gcm6h.js} +1 -1
  19. package/out/_next/static/chunks/0gwian.hp3-92.js +1 -0
  20. package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
  21. package/out/_next/static/chunks/0l5_.uqb-uqb8.js +1 -0
  22. package/out/_next/static/chunks/0mex8svsiv-2l.js +1 -0
  23. package/out/_next/static/chunks/0myq9gs8szydh.js +1 -0
  24. package/out/_next/static/chunks/{0usvo~vu7r8np.js → 0o9ce4cyf76by.js} +1 -1
  25. package/out/_next/static/chunks/0p0sv~fuddvgr.js +1 -0
  26. package/out/_next/static/chunks/{0o98f1yq..o.8.js → 0pt.5cg1t09qs.js} +1 -1
  27. package/out/_next/static/chunks/0q0ksgxg98xgd.js +1 -0
  28. package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
  29. package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
  30. package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
  31. package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
  32. package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
  33. package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
  34. package/out/_next/static/chunks/14_inksek_rth.js +2 -0
  35. package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
  36. package/out/_next/static/chunks/16xls5tt_68lx.js +1 -0
  37. package/out/_next/static/chunks/{turbopack-0xs6mybc~5t_3.js → turbopack-0xta0kqwzkf28.js} +1 -1
  38. package/out/_not-found/__next._full.txt +13 -13
  39. package/out/_not-found/__next._head.txt +3 -3
  40. package/out/_not-found/__next._index.txt +8 -8
  41. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  42. package/out/_not-found/__next._not-found.txt +3 -3
  43. package/out/_not-found/__next._tree.txt +3 -3
  44. package/out/_not-found/index.html +2 -2
  45. package/out/_not-found/index.txt +13 -13
  46. package/out/admin/__next._full.txt +15 -15
  47. package/out/admin/__next._head.txt +3 -3
  48. package/out/admin/__next._index.txt +8 -8
  49. package/out/admin/__next._tree.txt +4 -4
  50. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  51. package/out/admin/__next.admin.txt +4 -4
  52. package/out/admin/index.html +2 -2
  53. package/out/admin/index.txt +15 -15
  54. package/out/app/__next._full.txt +14 -14
  55. package/out/app/__next._head.txt +3 -3
  56. package/out/app/__next._index.txt +8 -8
  57. package/out/app/__next._tree.txt +3 -3
  58. package/out/app/__next.app.__PAGE__.txt +4 -4
  59. package/out/app/__next.app.txt +3 -3
  60. package/out/app/index.html +2 -2
  61. package/out/app/index.txt +14 -14
  62. package/out/chat/__next._full.txt +15 -15
  63. package/out/chat/__next._head.txt +3 -3
  64. package/out/chat/__next._index.txt +8 -8
  65. package/out/chat/__next._tree.txt +4 -4
  66. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  67. package/out/chat/__next.chat.txt +4 -4
  68. package/out/chat/index.html +2 -2
  69. package/out/chat/index.txt +15 -15
  70. package/out/chat/join/__next._full.txt +25 -0
  71. package/out/chat/join/__next._head.txt +5 -0
  72. package/out/{changelog → chat/join}/__next._index.txt +8 -8
  73. package/out/chat/join/__next._tree.txt +5 -0
  74. package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
  75. package/out/chat/join/__next.chat.join.txt +5 -0
  76. package/out/chat/join/__next.chat.txt +5 -0
  77. package/out/chat/join/index.html +15 -0
  78. package/out/chat/join/index.txt +25 -0
  79. package/out/download/__next._full.txt +21 -21
  80. package/out/download/__next._head.txt +3 -3
  81. package/out/download/__next._index.txt +8 -8
  82. package/out/download/__next._tree.txt +5 -5
  83. package/out/download/__next.download.__PAGE__.txt +9 -9
  84. package/out/download/__next.download.txt +3 -3
  85. package/out/download/index.html +2 -2
  86. package/out/download/index.txt +21 -21
  87. package/out/index.html +2 -2
  88. package/out/index.txt +16 -16
  89. package/out/note/__next._full.txt +14 -14
  90. package/out/note/__next._head.txt +3 -3
  91. package/out/note/__next._index.txt +8 -8
  92. package/out/note/__next._tree.txt +3 -3
  93. package/out/note/__next.note.__PAGE__.txt +4 -4
  94. package/out/note/__next.note.txt +3 -3
  95. package/out/note/index.html +2 -2
  96. package/out/note/index.txt +14 -14
  97. package/out/ping/__next._full.txt +16 -16
  98. package/out/ping/__next._head.txt +3 -3
  99. package/out/ping/__next._index.txt +8 -8
  100. package/out/ping/__next._tree.txt +5 -5
  101. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  102. package/out/ping/__next.ping.txt +4 -4
  103. package/out/ping/index.html +2 -2
  104. package/out/ping/index.txt +16 -16
  105. package/out/web3/__next._full.txt +15 -15
  106. package/out/web3/__next._head.txt +3 -3
  107. package/out/web3/__next._index.txt +8 -8
  108. package/out/web3/__next._tree.txt +4 -4
  109. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  110. package/out/web3/__next.web3.txt +4 -4
  111. package/out/web3/ed25519/__next._full.txt +13 -13
  112. package/out/web3/ed25519/__next._head.txt +3 -3
  113. package/out/web3/ed25519/__next._index.txt +8 -8
  114. package/out/web3/ed25519/__next._tree.txt +4 -4
  115. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  116. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  117. package/out/web3/ed25519/__next.web3.txt +4 -4
  118. package/out/web3/ed25519/index.html +1 -1
  119. package/out/web3/ed25519/index.txt +13 -13
  120. package/out/web3/index.html +2 -2
  121. package/out/web3/index.txt +15 -15
  122. package/out/web3/tools/__next._full.txt +13 -13
  123. package/out/web3/tools/__next._head.txt +3 -3
  124. package/out/web3/tools/__next._index.txt +8 -8
  125. package/out/web3/tools/__next._tree.txt +4 -4
  126. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  127. package/out/web3/tools/__next.web3.tools.txt +3 -3
  128. package/out/web3/tools/__next.web3.txt +4 -4
  129. package/out/web3/tools/index.html +1 -1
  130. package/out/web3/tools/index.txt +13 -13
  131. package/package.json +19 -9
  132. package/server/index.js +133 -1303
  133. package/server/src/config.js +1 -1
  134. package/server/src/core/channelAttachment.js +68 -0
  135. package/server/src/core/cid.js +2 -88
  136. package/server/src/core/cidTopic.js +29 -0
  137. package/server/src/core/mostLink.js +88 -0
  138. package/server/src/http/access.js +123 -0
  139. package/server/src/http/app.js +1095 -0
  140. package/server/src/http/errors.js +35 -0
  141. package/server/src/http/nodeLogs.js +53 -0
  142. package/server/src/http/nodeStatus.js +146 -0
  143. package/server/src/http/staticFiles.js +84 -0
  144. package/server/src/http/uploads.js +114 -0
  145. package/server/src/index.js +799 -211
  146. package/server/src/node/config.js +38 -6
  147. package/server/src/utils/api.js +287 -14
  148. package/server/src/utils/auth.js +63 -0
  149. package/server/src/utils/dateTime.js +30 -0
  150. package/server/src/utils/downloadMessages.js +89 -0
  151. package/server/src/utils/errors.js +7 -0
  152. package/server/src/utils/mostWallet.js +151 -0
  153. package/server/src/utils/mp.js +2 -26
  154. package/server/src/utils/noteBackup.js +2 -5
  155. package/server/src/utils/noteUtils.js +11 -3
  156. package/server/src/utils/userIdentity.js +0 -1
  157. package/out/_next/static/chunks/00-u5nq76f0.j.js +0 -1
  158. package/out/_next/static/chunks/00fm8lijienf1.js +0 -1
  159. package/out/_next/static/chunks/00o9ht.f2qm00.css +0 -4
  160. package/out/_next/static/chunks/00zi-erhjrny2.js +0 -2
  161. package/out/_next/static/chunks/084xf0edl9sfo.js +0 -1
  162. package/out/_next/static/chunks/09f1gfke9m5wg.css +0 -1
  163. package/out/_next/static/chunks/09xyi6fpro_d-.css +0 -1
  164. package/out/_next/static/chunks/0_npg_pcoywti.js +0 -5
  165. package/out/_next/static/chunks/0_r_mk1~6bosc.js +0 -1
  166. package/out/_next/static/chunks/0arm0a6adt7cc.css +0 -1
  167. package/out/_next/static/chunks/0c9j3eq_14vv2.css +0 -1
  168. package/out/_next/static/chunks/0d4bueddmcnca.js +0 -1
  169. package/out/_next/static/chunks/0gtwvy1z9ksa7.css +0 -1
  170. package/out/_next/static/chunks/0j27tcmtt4ly7.js +0 -1
  171. package/out/_next/static/chunks/0j3v4mq67wtnh.js +0 -1
  172. package/out/_next/static/chunks/0lkmf5ry.s_7w.js +0 -1
  173. package/out/_next/static/chunks/0p486m03-zfoi.js +0 -1
  174. package/out/_next/static/chunks/0r1~k82nji8sf.js +0 -1
  175. package/out/_next/static/chunks/0v7qp4hv-_._r.js +0 -1
  176. package/out/_next/static/chunks/0wuwlgcn6gxqt.js +0 -1
  177. package/out/_next/static/chunks/0xl5_avhu._i8.js +0 -1
  178. package/out/_next/static/chunks/10kvl8vj_plm-.js +0 -1
  179. package/out/_next/static/chunks/16m27azcs4k6w.js +0 -1
  180. package/out/changelog/__next._full.txt +0 -25
  181. package/out/changelog/__next._head.txt +0 -5
  182. package/out/changelog/__next._tree.txt +0 -5
  183. package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
  184. package/out/changelog/__next.changelog.txt +0 -5
  185. package/out/changelog/index.html +0 -15
  186. package/out/changelog/index.txt +0 -25
  187. package/out/docs/__next._full.txt +0 -25
  188. package/out/docs/__next._head.txt +0 -5
  189. package/out/docs/__next._index.txt +0 -9
  190. package/out/docs/__next._tree.txt +0 -5
  191. package/out/docs/__next.docs.__PAGE__.txt +0 -10
  192. package/out/docs/__next.docs.txt +0 -5
  193. package/out/docs/getting-started/__next._full.txt +0 -25
  194. package/out/docs/getting-started/__next._head.txt +0 -5
  195. package/out/docs/getting-started/__next._index.txt +0 -9
  196. package/out/docs/getting-started/__next._tree.txt +0 -5
  197. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
  198. package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
  199. package/out/docs/getting-started/__next.docs.txt +0 -5
  200. package/out/docs/getting-started/index.html +0 -15
  201. package/out/docs/getting-started/index.txt +0 -25
  202. package/out/docs/index.html +0 -15
  203. package/out/docs/index.txt +0 -25
  204. package/out/note/edit/__next._full.txt +0 -24
  205. package/out/note/edit/__next._head.txt +0 -5
  206. package/out/note/edit/__next._index.txt +0 -9
  207. package/out/note/edit/__next._tree.txt +0 -4
  208. package/out/note/edit/__next.note.edit.__PAGE__.txt +0 -9
  209. package/out/note/edit/__next.note.edit.txt +0 -5
  210. package/out/note/edit/__next.note.txt +0 -5
  211. package/out/note/edit/index.html +0 -15
  212. package/out/note/edit/index.txt +0 -24
  213. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_buildManifest.js +0 -0
  214. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_clientMiddlewareManifest.js +0 -0
  215. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_ssgManifest.js +0 -0
package/server/index.js CHANGED
@@ -1,1280 +1,47 @@
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'
16
7
  import {
8
+ DEFAULT_NODE_HOST,
9
+ DEFAULT_NODE_PORT,
17
10
  createNodeConfigStore,
18
- evaluateStorageLimits,
19
11
  } from './src/node/config.js'
20
12
  import { createNodeLogger } from './src/node/logs.js'
13
+ import {
14
+ UPLOAD_TMP_DIR,
15
+ createApp,
16
+ getDataPath,
17
+ } from './src/http/app.js'
21
18
 
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
- })
1028
-
1029
- // --- 显示名路由 ---
1030
- app.get('/api/display-name', c => {
1031
- return c.json({ displayName: engine.getDisplayName() })
1032
- })
1033
-
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
- })
1049
-
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
- }
1056
- try {
1057
- const result = await engine.createChannel(
1058
- body.name.trim(),
1059
- body.type || 'personal'
1060
- )
1061
- return c.json({ success: true, ...result })
1062
- } 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
- })
19
+ export { createApp } from './src/http/app.js'
1092
20
 
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
- })
21
+ const PORT = DEFAULT_NODE_PORT
22
+ const HOST = DEFAULT_NODE_HOST
1120
23
 
1121
- app.get('/api/channels/:name/peers', c => {
1122
- return c.json(engine.getChannelPeers(c.req.param('name')))
1123
- })
24
+ function cleanUploadTempDir() {
25
+ if (!fs.existsSync(UPLOAD_TMP_DIR)) return
1124
26
 
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
- }
27
+ const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
28
+ for (const file of staleFiles) {
1137
29
  try {
1138
- const result = engine.renameFolder(body.oldPath, body.newPath)
1139
- return c.json({ success: true, ...result })
30
+ fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file))
1140
31
  } 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
- }
1206
- }
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'))
32
+ console.warn('[MostBox] Failed to clean upload temp file:', err.message)
1212
33
  }
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
34
  }
35
+ console.log(`[MostBox] Cleaned ${staleFiles.length} stale upload temp files`)
1227
36
  }
1228
37
 
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
-
38
+ function bindEngineEvents({
39
+ engine,
40
+ wsBroadcast,
41
+ wsSendToChannel,
42
+ appendNodeLog,
43
+ broadcastNodeStatus,
44
+ }) {
1278
45
  let engineReadyForStatus = false
1279
46
  const safeBroadcastNodeStatus = () => {
1280
47
  if (engineReadyForStatus) {
@@ -1334,6 +101,10 @@ export async function main() {
1334
101
  })
1335
102
  safeBroadcastNodeStatus()
1336
103
  })
104
+ engine.on('seed:metrics', data => {
105
+ wsBroadcast('seed:metrics', data)
106
+ safeBroadcastNodeStatus()
107
+ })
1337
108
  engine.on('channel:message', data =>
1338
109
  wsSendToChannel(data.channel, 'channel:message', data)
1339
110
  )
@@ -1346,41 +117,23 @@ export async function main() {
1346
117
  engine.on('channel:joined', data => wsBroadcast('channel:joined', data))
1347
118
  engine.on('channel:left', data => wsBroadcast('channel:left', data))
1348
119
 
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}`)
120
+ return {
121
+ markReady() {
122
+ engineReadyForStatus = true
123
+ },
124
+ }
125
+ }
1364
126
 
1365
- if (process.env.ELECTRON_APP) return
127
+ function createWebSocketServer({
128
+ serverInstance,
129
+ validateWebSocketRequest,
130
+ subscribeToChannel,
131
+ unsubscribeFromChannel,
132
+ cleanupWsSubscriptions,
133
+ }) {
134
+ const wss = new WebSocketServer({ noServer: true })
1366
135
 
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
- )
1381
-
1382
- wssRef.current = new WebSocketServer({ noServer: true })
1383
- wssRef.current.on('connection', ws => {
136
+ wss.on('connection', ws => {
1384
137
  ws.on('error', () => {})
1385
138
  ws.on('close', () => {
1386
139
  cleanupWsSubscriptions(ws)
@@ -1411,31 +164,108 @@ export async function main() {
1411
164
  })
1412
165
  })
1413
166
 
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 {
167
+ serverInstance.on('upgrade', (req, socket, head) => {
168
+ if (!req.url.startsWith('/ws')) {
169
+ socket.destroy()
170
+ return
171
+ }
172
+
173
+ if (!validateWebSocketRequest(req)) {
1420
174
  socket.destroy()
175
+ return
1421
176
  }
177
+
178
+ wss.handleUpgrade(req, socket, head, ws => {
179
+ wss.emit('connection', ws, req)
180
+ })
1422
181
  })
1423
182
 
1424
- process.on('SIGINT', async () => {
1425
- console.log('\n[MostBox] Shutting down...')
183
+ return wss
184
+ }
185
+
186
+ function bindShutdownSignals({ engine, wssRef, serverInstanceRef }) {
187
+ async function shutdown(message) {
188
+ if (message) console.log(message)
1426
189
  await engine.stop()
1427
190
  if (wssRef.current) wssRef.current.close()
1428
191
  serverInstanceRef.current.close()
1429
192
  process.exit(0)
193
+ }
194
+
195
+ process.on('SIGINT', () => {
196
+ shutdown('\n[MostBox] Shutting down...')
1430
197
  })
1431
198
 
1432
- process.on('SIGTERM', async () => {
1433
- await engine.stop()
1434
- if (wssRef.current) wssRef.current.close()
1435
- serverInstanceRef.current.close()
1436
- process.exit(0)
199
+ process.on('SIGTERM', () => {
200
+ shutdown()
201
+ })
202
+ }
203
+
204
+ // --- 主函数 ---
205
+ export async function main() {
206
+ console.log('[MostBox] Starting core daemon...')
207
+ cleanUploadTempDir()
208
+
209
+ const configStore = createNodeConfigStore()
210
+ const nodeLogger = createNodeLogger(configStore.configDir)
211
+ const dataPath = getDataPath(configStore)
212
+ console.log(`[MostBox] Storage: ${dataPath}`)
213
+
214
+ const nodeConfig = configStore.getNodeConfig()
215
+ const engine = new MostBoxEngine({
216
+ dataPath,
217
+ maxFileSize: nodeConfig.maxFileSizeBytes,
218
+ capacityBytes: nodeConfig.capacityBytes,
219
+ })
220
+
221
+ const wssRef = { current: null }
222
+ const serverInstanceRef = { current: null }
223
+
224
+ const appRuntime = createApp(engine, {
225
+ port: PORT,
226
+ host: HOST,
227
+ configStore,
228
+ nodeLogger,
229
+ wssRef,
230
+ serverInstanceRef,
231
+ })
232
+
233
+ const engineEvents = bindEngineEvents({
234
+ engine,
235
+ wsBroadcast: appRuntime.wsBroadcast,
236
+ wsSendToChannel: appRuntime.wsSendToChannel,
237
+ appendNodeLog: appRuntime.appendNodeLog,
238
+ broadcastNodeStatus: appRuntime.broadcastNodeStatus,
239
+ })
240
+
241
+ await engine.start()
242
+ engineEvents.markReady()
243
+ console.log('[MostBox] Engine ready')
244
+ appRuntime.appendNodeLog({
245
+ event: 'node:ready',
246
+ message: 'Node daemon ready',
247
+ data: { dataPath, port: PORT },
248
+ })
249
+ appRuntime.broadcastNodeStatus()
250
+
251
+ serverInstanceRef.current = serve(
252
+ { fetch: appRuntime.app.fetch, port: PORT, hostname: HOST },
253
+ () => {
254
+ const displayUrl = `http://localhost:${PORT}`
255
+ console.log(`[MostBox] Server running at ${displayUrl}`)
256
+ }
257
+ )
258
+
259
+ wssRef.current = createWebSocketServer({
260
+ serverInstance: serverInstanceRef.current,
261
+ validateWebSocketRequest: appRuntime.validateWebSocketRequest,
262
+ subscribeToChannel: appRuntime.subscribeToChannel,
263
+ unsubscribeFromChannel: appRuntime.unsubscribeFromChannel,
264
+ cleanupWsSubscriptions: appRuntime.cleanupWsSubscriptions,
1437
265
  })
1438
266
 
267
+ bindShutdownSignals({ engine, wssRef, serverInstanceRef })
268
+
1439
269
  return engine
1440
270
  }
1441
271