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.
- package/README.md +21 -14
- package/electron/main.js +72 -8
- package/electron/preload.js +6 -0
- package/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +6 -6
- package/out/__next._full.txt +16 -16
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +8 -8
- package/out/__next._tree.txt +5 -5
- package/out/_next/static/chunks/0.e2avjgna_b2.js +1 -0
- package/out/_next/static/chunks/{0ho~log~~-jwp.css → 03h~nhgj0hv3p.css} +1 -1
- package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
- package/out/_next/static/chunks/{0qou.u2e2dy48.css → 0adx~d-j05c9d.css} +2 -2
- package/out/_next/static/chunks/0aq.rc9woa2nz.js +1 -0
- package/out/_next/static/chunks/0etes81d_cihn.js +1 -0
- package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
- package/out/_next/static/chunks/{0o6lrkxy4jwag.js → 0gcsdf57gcm6h.js} +1 -1
- package/out/_next/static/chunks/0gwian.hp3-92.js +1 -0
- package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
- package/out/_next/static/chunks/0l5_.uqb-uqb8.js +1 -0
- package/out/_next/static/chunks/0mex8svsiv-2l.js +1 -0
- package/out/_next/static/chunks/0myq9gs8szydh.js +1 -0
- package/out/_next/static/chunks/{0usvo~vu7r8np.js → 0o9ce4cyf76by.js} +1 -1
- package/out/_next/static/chunks/0p0sv~fuddvgr.js +1 -0
- package/out/_next/static/chunks/{0o98f1yq..o.8.js → 0pt.5cg1t09qs.js} +1 -1
- package/out/_next/static/chunks/0q0ksgxg98xgd.js +1 -0
- package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
- package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
- package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
- package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
- package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
- package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
- package/out/_next/static/chunks/14_inksek_rth.js +2 -0
- package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
- package/out/_next/static/chunks/16xls5tt_68lx.js +1 -0
- package/out/_next/static/chunks/{turbopack-0xs6mybc~5t_3.js → turbopack-0xta0kqwzkf28.js} +1 -1
- package/out/_not-found/__next._full.txt +13 -13
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +8 -8
- package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +3 -3
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +13 -13
- package/out/admin/__next._full.txt +15 -15
- package/out/admin/__next._head.txt +3 -3
- package/out/admin/__next._index.txt +8 -8
- package/out/admin/__next._tree.txt +4 -4
- package/out/admin/__next.admin.__PAGE__.txt +4 -4
- package/out/admin/__next.admin.txt +4 -4
- package/out/admin/index.html +2 -2
- package/out/admin/index.txt +15 -15
- package/out/app/__next._full.txt +14 -14
- package/out/app/__next._head.txt +3 -3
- package/out/app/__next._index.txt +8 -8
- package/out/app/__next._tree.txt +3 -3
- package/out/app/__next.app.__PAGE__.txt +4 -4
- package/out/app/__next.app.txt +3 -3
- package/out/app/index.html +2 -2
- package/out/app/index.txt +14 -14
- package/out/chat/__next._full.txt +15 -15
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +8 -8
- package/out/chat/__next._tree.txt +4 -4
- package/out/chat/__next.chat.__PAGE__.txt +4 -4
- package/out/chat/__next.chat.txt +4 -4
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +15 -15
- package/out/chat/join/__next._full.txt +25 -0
- package/out/chat/join/__next._head.txt +5 -0
- package/out/{changelog → chat/join}/__next._index.txt +8 -8
- package/out/chat/join/__next._tree.txt +5 -0
- package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
- package/out/chat/join/__next.chat.join.txt +5 -0
- package/out/chat/join/__next.chat.txt +5 -0
- package/out/chat/join/index.html +15 -0
- package/out/chat/join/index.txt +25 -0
- package/out/download/__next._full.txt +21 -21
- package/out/download/__next._head.txt +3 -3
- package/out/download/__next._index.txt +8 -8
- package/out/download/__next._tree.txt +5 -5
- package/out/download/__next.download.__PAGE__.txt +9 -9
- package/out/download/__next.download.txt +3 -3
- package/out/download/index.html +2 -2
- package/out/download/index.txt +21 -21
- package/out/index.html +2 -2
- package/out/index.txt +16 -16
- package/out/note/__next._full.txt +14 -14
- package/out/note/__next._head.txt +3 -3
- package/out/note/__next._index.txt +8 -8
- package/out/note/__next._tree.txt +3 -3
- package/out/note/__next.note.__PAGE__.txt +4 -4
- package/out/note/__next.note.txt +3 -3
- package/out/note/index.html +2 -2
- package/out/note/index.txt +14 -14
- package/out/ping/__next._full.txt +16 -16
- package/out/ping/__next._head.txt +3 -3
- package/out/ping/__next._index.txt +8 -8
- package/out/ping/__next._tree.txt +5 -5
- package/out/ping/__next.ping.__PAGE__.txt +5 -5
- package/out/ping/__next.ping.txt +4 -4
- package/out/ping/index.html +2 -2
- package/out/ping/index.txt +16 -16
- package/out/web3/__next._full.txt +15 -15
- package/out/web3/__next._head.txt +3 -3
- package/out/web3/__next._index.txt +8 -8
- package/out/web3/__next._tree.txt +4 -4
- package/out/web3/__next.web3.__PAGE__.txt +4 -4
- package/out/web3/__next.web3.txt +4 -4
- package/out/web3/ed25519/__next._full.txt +13 -13
- package/out/web3/ed25519/__next._head.txt +3 -3
- package/out/web3/ed25519/__next._index.txt +8 -8
- package/out/web3/ed25519/__next._tree.txt +4 -4
- package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
- package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
- package/out/web3/ed25519/__next.web3.txt +4 -4
- package/out/web3/ed25519/index.html +1 -1
- package/out/web3/ed25519/index.txt +13 -13
- package/out/web3/index.html +2 -2
- package/out/web3/index.txt +15 -15
- package/out/web3/tools/__next._full.txt +13 -13
- package/out/web3/tools/__next._head.txt +3 -3
- package/out/web3/tools/__next._index.txt +8 -8
- package/out/web3/tools/__next._tree.txt +4 -4
- package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
- package/out/web3/tools/__next.web3.tools.txt +3 -3
- package/out/web3/tools/__next.web3.txt +4 -4
- package/out/web3/tools/index.html +1 -1
- package/out/web3/tools/index.txt +13 -13
- package/package.json +19 -9
- package/server/index.js +133 -1303
- package/server/src/config.js +1 -1
- package/server/src/core/channelAttachment.js +68 -0
- package/server/src/core/cid.js +2 -88
- package/server/src/core/cidTopic.js +29 -0
- package/server/src/core/mostLink.js +88 -0
- package/server/src/http/access.js +123 -0
- package/server/src/http/app.js +1095 -0
- package/server/src/http/errors.js +35 -0
- package/server/src/http/nodeLogs.js +53 -0
- package/server/src/http/nodeStatus.js +146 -0
- package/server/src/http/staticFiles.js +84 -0
- package/server/src/http/uploads.js +114 -0
- package/server/src/index.js +799 -211
- package/server/src/node/config.js +38 -6
- package/server/src/utils/api.js +287 -14
- package/server/src/utils/auth.js +63 -0
- package/server/src/utils/dateTime.js +30 -0
- package/server/src/utils/downloadMessages.js +89 -0
- package/server/src/utils/errors.js +7 -0
- package/server/src/utils/mostWallet.js +151 -0
- package/server/src/utils/mp.js +2 -26
- package/server/src/utils/noteBackup.js +2 -5
- package/server/src/utils/noteUtils.js +11 -3
- package/server/src/utils/userIdentity.js +0 -1
- package/out/_next/static/chunks/00-u5nq76f0.j.js +0 -1
- package/out/_next/static/chunks/00fm8lijienf1.js +0 -1
- package/out/_next/static/chunks/00o9ht.f2qm00.css +0 -4
- package/out/_next/static/chunks/00zi-erhjrny2.js +0 -2
- package/out/_next/static/chunks/084xf0edl9sfo.js +0 -1
- package/out/_next/static/chunks/09f1gfke9m5wg.css +0 -1
- package/out/_next/static/chunks/09xyi6fpro_d-.css +0 -1
- package/out/_next/static/chunks/0_npg_pcoywti.js +0 -5
- package/out/_next/static/chunks/0_r_mk1~6bosc.js +0 -1
- package/out/_next/static/chunks/0arm0a6adt7cc.css +0 -1
- package/out/_next/static/chunks/0c9j3eq_14vv2.css +0 -1
- package/out/_next/static/chunks/0d4bueddmcnca.js +0 -1
- package/out/_next/static/chunks/0gtwvy1z9ksa7.css +0 -1
- package/out/_next/static/chunks/0j27tcmtt4ly7.js +0 -1
- package/out/_next/static/chunks/0j3v4mq67wtnh.js +0 -1
- package/out/_next/static/chunks/0lkmf5ry.s_7w.js +0 -1
- package/out/_next/static/chunks/0p486m03-zfoi.js +0 -1
- package/out/_next/static/chunks/0r1~k82nji8sf.js +0 -1
- package/out/_next/static/chunks/0v7qp4hv-_._r.js +0 -1
- package/out/_next/static/chunks/0wuwlgcn6gxqt.js +0 -1
- package/out/_next/static/chunks/0xl5_avhu._i8.js +0 -1
- package/out/_next/static/chunks/10kvl8vj_plm-.js +0 -1
- package/out/_next/static/chunks/16m27azcs4k6w.js +0 -1
- package/out/changelog/__next._full.txt +0 -25
- package/out/changelog/__next._head.txt +0 -5
- package/out/changelog/__next._tree.txt +0 -5
- package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
- package/out/changelog/__next.changelog.txt +0 -5
- package/out/changelog/index.html +0 -15
- package/out/changelog/index.txt +0 -25
- package/out/docs/__next._full.txt +0 -25
- package/out/docs/__next._head.txt +0 -5
- package/out/docs/__next._index.txt +0 -9
- package/out/docs/__next._tree.txt +0 -5
- package/out/docs/__next.docs.__PAGE__.txt +0 -10
- package/out/docs/__next.docs.txt +0 -5
- package/out/docs/getting-started/__next._full.txt +0 -25
- package/out/docs/getting-started/__next._head.txt +0 -5
- package/out/docs/getting-started/__next._index.txt +0 -9
- package/out/docs/getting-started/__next._tree.txt +0 -5
- package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
- package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
- package/out/docs/getting-started/__next.docs.txt +0 -5
- package/out/docs/getting-started/index.html +0 -15
- package/out/docs/getting-started/index.txt +0 -25
- package/out/docs/index.html +0 -15
- package/out/docs/index.txt +0 -25
- package/out/note/edit/__next._full.txt +0 -24
- package/out/note/edit/__next._head.txt +0 -5
- package/out/note/edit/__next._index.txt +0 -9
- package/out/note/edit/__next._tree.txt +0 -4
- package/out/note/edit/__next.note.edit.__PAGE__.txt +0 -9
- package/out/note/edit/__next.note.edit.txt +0 -5
- package/out/note/edit/__next.note.txt +0 -5
- package/out/note/edit/index.html +0 -15
- package/out/note/edit/index.txt +0 -24
- /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_buildManifest.js +0 -0
- /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_clientMiddlewareManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
})
|
|
24
|
+
function cleanUploadTempDir() {
|
|
25
|
+
if (!fs.existsSync(UPLOAD_TMP_DIR)) return
|
|
1124
26
|
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
1139
|
-
return c.json({ success: true, ...result })
|
|
30
|
+
fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file))
|
|
1140
31
|
} catch (err) {
|
|
1141
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
127
|
+
function createWebSocketServer({
|
|
128
|
+
serverInstance,
|
|
129
|
+
validateWebSocketRequest,
|
|
130
|
+
subscribeToChannel,
|
|
131
|
+
unsubscribeFromChannel,
|
|
132
|
+
cleanupWsSubscriptions,
|
|
133
|
+
}) {
|
|
134
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
1366
135
|
|
|
1367
|
-
|
|
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
|
-
|
|
1415
|
-
if (req.url.startsWith('/ws')) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
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',
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
|