most-box 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/out/about/index.html +20 -0
  2. package/out/admin/index.html +0 -0
  3. package/out/app/index.html +0 -0
  4. package/out/assets/AccountMenu-B26qus-Q.js +1 -0
  5. package/out/assets/AppShell-BiYQsNO8.js +1 -0
  6. package/out/assets/AppTop-D-qYC0ep.js +1 -0
  7. package/out/assets/ChatRestoringIndicator-CLV78gNW.js +1 -0
  8. package/out/assets/CopyButton-BrEkzqEq.js +1 -0
  9. package/out/assets/MarketingHeader-CJXpogPY.js +1 -0
  10. package/out/assets/MarketingLayout-Ch4sw5Qm.js +1 -0
  11. package/out/assets/{MilkdownEditor-BzbYt-xb.js → MilkdownEditor-DfhuT9eU.js} +1 -1
  12. package/out/assets/OpenSidebarButton-Bvwr7B6k.js +1 -0
  13. package/out/assets/arrow-right-BsHzC0ZL.js +1 -0
  14. package/out/assets/{arrow-up-down-B-677ip8.js → arrow-up-down-DvLSz6Ma.js} +1 -1
  15. package/out/assets/{userProfile-BZvkCuet.js → channelApi-C5eHetoJ.js} +1 -1
  16. package/out/assets/chatJoinInvite-7tuD-STm.js +1 -0
  17. package/out/assets/{chatNoteDraft-CYbvcEvC.js → chatNoteDraft-DTy6In5R.js} +1 -1
  18. package/out/assets/{circle-alert-dfN4T6Nq.js → circle-alert-DXVkx8p8.js} +1 -1
  19. package/out/assets/{copy-B7pPZ2MP.js → copy-OwXDsstV.js} +1 -1
  20. package/out/assets/{download-COQOcPqI.js → download-Cy2BlfKM.js} +1 -1
  21. package/out/assets/downloadValidation-Dd43i9_9.js +1 -0
  22. package/out/assets/{external-link-D6tHlYgS.js → external-link-D_AQSgxW.js} +1 -1
  23. package/out/assets/{file-text-DiyIGL1I.js → file-text-zvTER2mN.js} +1 -1
  24. package/out/assets/fingerprint-pattern-BT73ASEB.js +1 -0
  25. package/out/assets/{folder-BS_GbpPQ.js → folder-6pYYnS2-.js} +1 -1
  26. package/out/assets/folder-open-DOJ4bB58.js +1 -0
  27. package/out/assets/format-CAKfypjK.js +1 -0
  28. package/out/assets/gamepad-2-BYqNgCQr.js +1 -0
  29. package/out/assets/{hard-drive-Cqa7Rstj.js → hard-drive-DL1KJHmG.js} +1 -1
  30. package/out/assets/i18n-DKsg67mH.js +1 -0
  31. package/out/assets/index-FEwag7ej.css +1 -0
  32. package/out/assets/index-mjFWLwue.js +31 -0
  33. package/out/assets/index.lazy-8GSHLIGt.js +3 -0
  34. package/out/assets/index.lazy-B3jH9yBA.js +1 -0
  35. package/out/assets/index.lazy-B5JMiv3z.js +1 -0
  36. package/out/assets/index.lazy-BAr7-dhf.js +1 -0
  37. package/out/assets/index.lazy-BMFwyE_Z.js +3 -0
  38. package/out/assets/index.lazy-BbA_1LnY.js +1 -0
  39. package/out/assets/index.lazy-BmpOpQqg.js +1 -0
  40. package/out/assets/index.lazy-CB3C6kMj.js +1 -0
  41. package/out/assets/index.lazy-CVOAOO0_.js +1 -0
  42. package/out/assets/index.lazy-CyFSwJuo.js +1 -0
  43. package/out/assets/index.lazy-D3iEq2ZW.js +2 -0
  44. package/out/assets/index.lazy-F3iHN_Ao.js +1 -0
  45. package/out/assets/index.lazy-PnZR3z1V.js +5 -0
  46. package/out/assets/index.lazy-qDpRLqXu.js +1 -0
  47. package/out/assets/index.lazy-s6Ok77GI.js +1 -0
  48. package/out/assets/{loader-CwyHYZdX.js → loader-C4pED_QP.js} +1 -1
  49. package/out/assets/lock-yG7M7GkB.js +1 -0
  50. package/out/assets/{message-square-BgsNXPft.js → message-square-CfApq8Ou.js} +1 -1
  51. package/out/assets/mp-cEQteKUC.js +1 -0
  52. package/out/assets/{music-Dn9nm9Qb.js → music-Br2x8TKX.js} +1 -1
  53. package/out/assets/notebook-pen-Bcd3MrtD.js +1 -0
  54. package/out/assets/play-DSHtNcOe.js +1 -0
  55. package/out/assets/{refresh-cw-D4BsHvhi.js → refresh-cw-CfkUlTK9.js} +1 -1
  56. package/out/assets/{save-B02sbO9Y.js → save-CfYzXcxH.js} +1 -1
  57. package/out/assets/saveLocalFile-BLTn3Z18.js +1 -0
  58. package/out/assets/search-DLBOUfny.js +1 -0
  59. package/out/assets/{send-4dhnJGuS.js → send-Bie9hgHI.js} +1 -1
  60. package/out/assets/{shield-check-Boae7Nc6.js → shield-check-2-h_eEXp.js} +1 -1
  61. package/out/assets/{smartphone-JXH-zQXB.js → smartphone-CrsocYJo.js} +1 -1
  62. package/out/assets/trash-2-B9gno5q6.js +1 -0
  63. package/out/assets/{triangle-alert-B9sMiA9l.js → triangle-alert-BhNtjKm2.js} +1 -1
  64. package/out/assets/{upload-DVuXJt_o.js → upload-BmufnMFu.js} +1 -1
  65. package/out/assets/{useAppStore-DOnk608w.js → useAppStore-DNhyhD20.js} +1 -1
  66. package/out/assets/useChannelMessages-DKlO_2oW.js +4 -0
  67. package/out/assets/useGameRoom-DLN636mz.js +1 -0
  68. package/out/assets/{wallet-D9Iw4YWE.js → wallet-D6HZocla.js} +1 -1
  69. package/out/assets/{wifi-ljEI_ySA.js → wifi-CILQyKk0.js} +1 -1
  70. package/out/chat/index.html +0 -0
  71. package/out/chat/join/demo/index.html +0 -0
  72. package/out/chat/join/index.html +0 -0
  73. package/out/download/index.html +2 -2
  74. package/out/game/gandengyan/index.html +0 -0
  75. package/out/game/index.html +0 -0
  76. package/out/game/zhajinhua/index.html +0 -0
  77. package/out/index.html +2 -2
  78. package/out/note/index.html +0 -0
  79. package/out/ping/index.html +2 -2
  80. package/out/profile/index.html +0 -0
  81. package/out/web3/index.html +0 -0
  82. package/package.json +1 -1
  83. package/server/index.js +18 -1
  84. package/server/src/core/channelVoice.js +164 -0
  85. package/server/src/core/mostLink.js +30 -13
  86. package/server/src/http/nodeStatus.js +2 -2
  87. package/server/src/http/routes/fileRoutes.js +10 -8
  88. package/server/src/http/uploads.js +53 -15
  89. package/server/src/index.js +317 -133
  90. package/server/src/node/config.js +4 -4
  91. package/server/src/utils/api.js +9 -1
  92. package/server/src/utils/avatar.js +1 -10
  93. package/server/src/utils/downloadMessages.js +7 -7
  94. package/server/src/utils/errors.js +11 -1
  95. package/server/src/utils/userIdentity.js +0 -1
  96. package/out/assets/AccountMenu-ptQklqvD.js +0 -1
  97. package/out/assets/AppTop-Cy0ZOCxO.js +0 -1
  98. package/out/assets/CopyButton-C8PIDSJZ.js +0 -1
  99. package/out/assets/FilePreviewOverlay-faaTnmlN.js +0 -1
  100. package/out/assets/MarketingHeader-CxJ_OVOW.js +0 -1
  101. package/out/assets/MarketingLayout-CiN5KYcE.js +0 -1
  102. package/out/assets/OpenSidebarButton-CGhXxQ5Z.js +0 -1
  103. package/out/assets/arrow-right-CXzRt0Fp.js +0 -1
  104. package/out/assets/chatJoinInvite-B-ucvYdg.js +0 -1
  105. package/out/assets/check-iBCQl0yr.js +0 -1
  106. package/out/assets/chevron-right-D75NRNNs.js +0 -1
  107. package/out/assets/downloadValidation-UPgpJbKz.js +0 -1
  108. package/out/assets/format-CR8oUWq6.js +0 -1
  109. package/out/assets/index-D0Uk9-58.js +0 -31
  110. package/out/assets/index-FV6RZlVI.css +0 -1
  111. package/out/assets/index.lazy-B6XDJ8fg.js +0 -1
  112. package/out/assets/index.lazy-BFdWbm3R.js +0 -1
  113. package/out/assets/index.lazy-BzIt7fP4.js +0 -1
  114. package/out/assets/index.lazy-CLkb32nv.js +0 -1
  115. package/out/assets/index.lazy-CNmqW4-r.js +0 -1
  116. package/out/assets/index.lazy-Cnkj-gAd.js +0 -3
  117. package/out/assets/index.lazy-Cx1bp84f.js +0 -5
  118. package/out/assets/index.lazy-CxLzR674.js +0 -2
  119. package/out/assets/index.lazy-D4RjTGu-.js +0 -3
  120. package/out/assets/index.lazy-D9xLqvqW.js +0 -1
  121. package/out/assets/index.lazy-DNCF6MTJ.js +0 -1
  122. package/out/assets/index.lazy-MY-mUG5Z.js +0 -1
  123. package/out/assets/index.lazy-erUKA2rI.js +0 -1
  124. package/out/assets/index.lazy-qTjgLKhY.js +0 -1
  125. package/out/assets/key-round-JR8M3aHl.js +0 -1
  126. package/out/assets/lock-CuwwlXlx.js +0 -1
  127. package/out/assets/mp-Dv12gDTx.js +0 -1
  128. package/out/assets/notebook-pen-BzrCXQJE.js +0 -1
  129. package/out/assets/play-CvWKSMwY.js +0 -1
  130. package/out/assets/search-HW4oPOBP.js +0 -1
  131. package/out/assets/trash-2-BLwoM0ME.js +0 -1
  132. package/out/assets/useChannelMessages-CDvw-f_x.js +0 -4
  133. package/out/assets/useGameRoom-CS1w4IqP.js +0 -1
package/server/index.js CHANGED
@@ -143,6 +143,9 @@ function bindEngineEvents({
143
143
  engine.on('channel:presence', data =>
144
144
  wsSendToChannel(data.channelKey, 'channel:presence', data)
145
145
  )
146
+ engine.on('channel:voice', data =>
147
+ wsSendToChannel(data.channelKey, 'channel:voice', data)
148
+ )
146
149
  engine.on('channel:joined', data => wsBroadcast('channel:joined', data))
147
150
  engine.on('channel:left', data => wsBroadcast('channel:left', data))
148
151
  engine.on('user:metadata:updated', data => {
@@ -162,7 +165,7 @@ function bindEngineEvents({
162
165
  }
163
166
  }
164
167
 
165
- function createWebSocketServer({
168
+ export function createWebSocketServer({
166
169
  engine,
167
170
  serverInstance,
168
171
  validateWebSocketRequest,
@@ -247,6 +250,20 @@ function createWebSocketServer({
247
250
  })
248
251
  }
249
252
  break
253
+ case 'channel:voice:join':
254
+ case 'channel:voice:state':
255
+ case 'channel:voice:heartbeat':
256
+ case 'channel:voice:leave':
257
+ case 'channel:voice:signal':
258
+ if (data.channel && ws.userAddress) {
259
+ engine.sendChannelVoiceEvent(data.channel, {
260
+ ...data,
261
+ event: event.replace('channel:voice:', ''),
262
+ }, {
263
+ ownerAddress: ws.userAddress,
264
+ })
265
+ }
266
+ break
250
267
  }
251
268
  } catch (err) {
252
269
  console.error('[WS Message Error]', err.message)
@@ -0,0 +1,164 @@
1
+ import {
2
+ buildChannelKey,
3
+ normalizeChannelAvatar,
4
+ normalizeChannelDisplayName,
5
+ normalizeChannelId,
6
+ } from './channelIdentity.js'
7
+ import { normalizeOwnerAddress } from './ownerMetadata.js'
8
+ import { ValidationError } from '../utils/errors.js'
9
+
10
+ export const CHANNEL_VOICE_EVENTS = new Set([
11
+ 'join',
12
+ 'state',
13
+ 'heartbeat',
14
+ 'leave',
15
+ 'signal',
16
+ ])
17
+
18
+ const CHANNEL_VOICE_SIGNAL_TYPES = new Set(['offer', 'answer', 'candidate'])
19
+ const CHANNEL_VOICE_SESSION_REGEX = /^[a-zA-Z0-9_-]{3,80}$/
20
+ const CHANNEL_VOICE_SIGNAL_MAX_BYTES = 64 * 1024
21
+
22
+ function normalizeSessionId(value, label = 'voice session') {
23
+ const sessionId = String(value || '').trim()
24
+ if (!CHANNEL_VOICE_SESSION_REGEX.test(sessionId)) {
25
+ throw new ValidationError(`Invalid ${label}`)
26
+ }
27
+ return sessionId
28
+ }
29
+
30
+ function assertPayloadSize(value, message) {
31
+ const size = Buffer.byteLength(JSON.stringify(value || {}), 'utf8')
32
+ if (size > CHANNEL_VOICE_SIGNAL_MAX_BYTES) {
33
+ throw new ValidationError(message)
34
+ }
35
+ }
36
+
37
+ function normalizeSignal(input) {
38
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
39
+ throw new ValidationError('voice signal is required')
40
+ }
41
+
42
+ assertPayloadSize(input, 'voice signal is too large')
43
+
44
+ const type = String(input.type || '').trim()
45
+ if (!CHANNEL_VOICE_SIGNAL_TYPES.has(type)) {
46
+ throw new ValidationError('Invalid voice signal type')
47
+ }
48
+
49
+ if (type === 'offer' || type === 'answer') {
50
+ const sdp = String(input.sdp || '')
51
+ if (!sdp.trim()) {
52
+ throw new ValidationError('voice signal SDP is required')
53
+ }
54
+ return { type, sdp }
55
+ }
56
+
57
+ if (
58
+ !input.candidate ||
59
+ typeof input.candidate !== 'object' ||
60
+ Array.isArray(input.candidate)
61
+ ) {
62
+ throw new ValidationError('voice signal candidate is required')
63
+ }
64
+
65
+ const candidate = {
66
+ candidate: String(input.candidate.candidate || ''),
67
+ sdpMid:
68
+ input.candidate.sdpMid === null || input.candidate.sdpMid === undefined
69
+ ? null
70
+ : String(input.candidate.sdpMid),
71
+ sdpMLineIndex:
72
+ input.candidate.sdpMLineIndex === null ||
73
+ input.candidate.sdpMLineIndex === undefined
74
+ ? null
75
+ : Number(input.candidate.sdpMLineIndex),
76
+ }
77
+ if (
78
+ input.candidate.usernameFragment !== null &&
79
+ input.candidate.usernameFragment !== undefined
80
+ ) {
81
+ candidate.usernameFragment = String(input.candidate.usernameFragment)
82
+ }
83
+
84
+ return {
85
+ type,
86
+ candidate,
87
+ }
88
+ }
89
+
90
+ export function normalizeChannelVoiceEvent(
91
+ channelInput,
92
+ input = {},
93
+ options = {}
94
+ ) {
95
+ const channelId = normalizeChannelId(channelInput)
96
+ if (!channelId) {
97
+ throw new ValidationError('voice channel is required')
98
+ }
99
+
100
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
101
+ const inputSender =
102
+ input.sender && typeof input.sender === 'object' && !Array.isArray(input.sender)
103
+ ? input.sender
104
+ : {}
105
+ const senderAddress = ownerAddress || normalizeOwnerAddress(inputSender.address)
106
+ if (!senderAddress) {
107
+ throw new ValidationError('voice sender is required')
108
+ }
109
+
110
+ const event = String(input.event || '').trim()
111
+ if (!CHANNEL_VOICE_EVENTS.has(event)) {
112
+ throw new ValidationError('Invalid voice event')
113
+ }
114
+
115
+ const sessionId = normalizeSessionId(input.sessionId)
116
+ const channelKey = buildChannelKey(channelId)
117
+ const normalized = {
118
+ channel: channelKey,
119
+ channelKey,
120
+ channelId,
121
+ event,
122
+ sessionId,
123
+ sender: {
124
+ address: senderAddress,
125
+ },
126
+ timestamp: Number(options.timestamp) || Date.now(),
127
+ }
128
+
129
+ const displayName = normalizeChannelDisplayName(
130
+ input.displayName ?? inputSender.displayName,
131
+ senderAddress
132
+ )
133
+ if (displayName) normalized.sender.displayName = displayName
134
+
135
+ const avatar = normalizeChannelAvatar(input.avatar ?? inputSender.avatar)
136
+ if (avatar) normalized.sender.avatar = avatar
137
+
138
+ const profileUpdatedAt = Number(
139
+ input.profileUpdatedAt ?? inputSender.profileUpdatedAt
140
+ )
141
+ if (Number.isFinite(profileUpdatedAt) && profileUpdatedAt > 0) {
142
+ normalized.sender.profileUpdatedAt = profileUpdatedAt
143
+ }
144
+
145
+ if (input.micMuted !== undefined) {
146
+ normalized.micMuted = Boolean(input.micMuted)
147
+ }
148
+
149
+ if (event === 'signal') {
150
+ normalized.targetSessionId = normalizeSessionId(
151
+ input.targetSessionId,
152
+ 'voice target session'
153
+ )
154
+ normalized.signal = normalizeSignal(input.signal)
155
+ } else if (input.targetSessionId) {
156
+ normalized.targetSessionId = normalizeSessionId(
157
+ input.targetSessionId,
158
+ 'voice target session'
159
+ )
160
+ }
161
+
162
+ assertPayloadSize(normalized, 'voice event is too large')
163
+ return normalized
164
+ }
@@ -24,6 +24,16 @@ function invalidLink(errorCode, details) {
24
24
  }
25
25
  }
26
26
 
27
+ function extractTailTarget(value) {
28
+ const queryStart = value.indexOf('?')
29
+ const pathPart = queryStart === -1 ? value : value.slice(0, queryStart)
30
+ const queryPart = queryStart === -1 ? '' : value.slice(queryStart)
31
+ const slashIndex = pathPart.lastIndexOf('/')
32
+ const tailPath = slashIndex === -1 ? pathPart : pathPart.slice(slashIndex + 1)
33
+
34
+ return `${tailPath}${queryPart}`
35
+ }
36
+
27
37
  /**
28
38
  * 验证 CID 字符串
29
39
  * @param {string} cidString - 要验证的 CID 字符串
@@ -62,27 +72,34 @@ export function parseMostLink(link) {
62
72
  return invalidLink(MOST_LINK_ERROR_CODES.LINK_EMPTY)
63
73
  }
64
74
 
65
- let url
66
- try {
67
- url = new URL(link)
68
- } catch {
69
- return invalidLink(MOST_LINK_ERROR_CODES.INVALID_URL)
75
+ const value = link.trim()
76
+ if (!value) {
77
+ return invalidLink(MOST_LINK_ERROR_CODES.LINK_EMPTY)
70
78
  }
71
79
 
72
- if (url.protocol !== 'most:') {
73
- return invalidLink(MOST_LINK_ERROR_CODES.INVALID_PROTOCOL)
74
- }
80
+ const tailTarget = extractTailTarget(value)
81
+ const queryStart = tailTarget.indexOf('?')
82
+ const cidString =
83
+ queryStart === -1 ? tailTarget : tailTarget.slice(0, queryStart)
84
+ const queryString =
85
+ queryStart === -1 ? '' : tailTarget.slice(queryStart + 1)
75
86
 
76
- if (url.pathname && url.pathname !== '/') {
77
- return invalidLink(MOST_LINK_ERROR_CODES.UNSUPPORTED_PATH)
87
+ if (!cidString) {
88
+ return invalidLink(MOST_LINK_ERROR_CODES.INVALID_CID_FORMAT)
78
89
  }
79
90
 
80
- const cidString = url.hostname
81
- const rawFileName = url.searchParams.get('filename')
82
- const unsupportedParam = [...url.searchParams.keys()].find(
91
+ const searchParams = new URLSearchParams(queryString)
92
+ const rawFileName = searchParams.get('filename')
93
+ const unsupportedParam = [...searchParams.keys()].find(
83
94
  key => key !== 'filename'
84
95
  )
85
96
 
97
+ if (queryString && ![...searchParams.keys()].length) {
98
+ return invalidLink(MOST_LINK_ERROR_CODES.UNSUPPORTED_QUERY_PARAM, {
99
+ param: '',
100
+ })
101
+ }
102
+
86
103
  const validation = validateCidString(cidString)
87
104
  if (!validation.valid) {
88
105
  return invalidLink(validation.errorCode)
@@ -214,13 +214,13 @@ export function buildOpenApiSpec(appPort) {
214
214
  '/api/download/check': {
215
215
  post: {
216
216
  summary:
217
- 'Check whether a most:// link is locally available or discoverable',
217
+ 'Check whether a CID-tailed share target is locally available or discoverable',
218
218
  responses: { 200: { description: 'Download availability result' } },
219
219
  },
220
220
  },
221
221
  '/api/download': {
222
222
  post: {
223
- summary: 'Start downloading a most:// link',
223
+ summary: 'Start downloading a CID-tailed share target',
224
224
  responses: { 200: { description: 'Download task result' } },
225
225
  },
226
226
  },
@@ -15,10 +15,15 @@ export function registerFileRoutes(app, { engine, configStore, wsBroadcast }) {
15
15
 
16
16
  app.post('/api/publish', async c => {
17
17
  const req = c.env.incoming
18
- const result = await parseMultipartBusboy(
19
- req,
20
- configStore.getNodeConfig().maxFileSizeBytes
21
- )
18
+ let result
19
+ try {
20
+ result = await parseMultipartBusboy(
21
+ req,
22
+ configStore.getNodeConfig().maxFileSizeBytes
23
+ )
24
+ } catch (err) {
25
+ return badRequestOrAppError(c, err)
26
+ }
22
27
 
23
28
  if (!result || !result.filename) {
24
29
  return c.json({ error: 'No file provided' }, 400)
@@ -56,11 +61,8 @@ export function registerFileRoutes(app, { engine, configStore, wsBroadcast }) {
56
61
  if (localAvailability) {
57
62
  return c.json({
58
63
  success: true,
59
- available: true,
60
- cid: parsed.cid,
61
- fileName: localAvailability.fileName,
64
+ ...localAvailability,
62
65
  size: Number(localAvailability.size) || null,
63
- alreadyExists: true,
64
66
  })
65
67
  }
66
68
 
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import os from 'node:os'
4
4
  import Busboy from 'busboy'
5
5
  import { MAX_FILE_SIZE } from '../config.js'
6
+ import { FileSizeError } from '../utils/errors.js'
6
7
 
7
8
  export const UPLOAD_TMP_DIR = path.join(os.tmpdir(), 'most-box-uploads')
8
9
 
@@ -56,10 +57,34 @@ export async function parseMultipartBusboy(req, maxUploadSize = MAX_FILE_SIZE) {
56
57
 
57
58
  const result = { filePath: null, filename: null }
58
59
  let fileSize = 0
60
+ let activeFileStream = null
59
61
  let writeStream = null
60
62
  let tempPath = null
63
+ let settled = false
64
+
65
+ function cleanupTempFile() {
66
+ if (tempPath) fs.unlink(tempPath, () => {})
67
+ }
68
+
69
+ function fail(err) {
70
+ if (settled) return
71
+ settled = true
72
+ if (activeFileStream && !activeFileStream.destroyed) {
73
+ activeFileStream.destroy()
74
+ }
75
+ if (writeStream && !writeStream.destroyed) writeStream.destroy()
76
+ cleanupTempFile()
77
+ reject(err)
78
+ }
79
+
80
+ function succeed(value) {
81
+ if (settled) return
82
+ settled = true
83
+ resolve(value)
84
+ }
61
85
 
62
86
  busboy.on('file', (name, stream, info) => {
87
+ activeFileStream = stream
63
88
  result.filename = decodeFilenameFromHeader(`filename="${info.filename}"`)
64
89
  tempPath = path.join(
65
90
  UPLOAD_TMP_DIR,
@@ -70,45 +95,58 @@ export async function parseMultipartBusboy(req, maxUploadSize = MAX_FILE_SIZE) {
70
95
  stream.on('data', chunk => {
71
96
  fileSize += chunk.length
72
97
  if (fileSize > maxUploadSize) {
73
- stream.destroy()
74
- writeStream.destroy()
75
- fs.unlink(tempPath, () => {})
76
- reject(new Error('File too large'))
77
- return
98
+ fail(new FileSizeError('File too large', fileSize, maxUploadSize))
78
99
  }
79
100
  })
80
101
 
81
- stream.on('error', () => {
82
- if (tempPath) fs.unlink(tempPath, () => {})
102
+ stream.on('limit', () => {
103
+ fail(
104
+ new FileSizeError(
105
+ 'File too large',
106
+ fileSize || maxUploadSize,
107
+ maxUploadSize
108
+ )
109
+ )
110
+ })
111
+
112
+ stream.on('error', err => {
113
+ fail(err)
83
114
  })
84
115
 
85
116
  stream.pipe(writeStream)
86
117
 
87
118
  writeStream.on('finish', () => {
119
+ if (stream.truncated) {
120
+ fail(
121
+ new FileSizeError(
122
+ 'File too large',
123
+ fileSize || maxUploadSize,
124
+ maxUploadSize
125
+ )
126
+ )
127
+ return
128
+ }
88
129
  result.filePath = tempPath
89
- resolve(result)
130
+ succeed(result)
90
131
  })
91
132
 
92
133
  writeStream.on('error', err => {
93
- if (tempPath) fs.unlink(tempPath, () => {})
94
- reject(err)
134
+ fail(err)
95
135
  })
96
136
  })
97
137
 
98
138
  busboy.on('error', err => {
99
- if (tempPath) fs.unlink(tempPath, () => {})
100
- reject(err)
139
+ fail(err)
101
140
  })
102
141
 
103
142
  busboy.on('close', () => {
104
143
  if (!result.filename) {
105
- resolve(null)
144
+ succeed(null)
106
145
  }
107
146
  })
108
147
 
109
148
  req.on('error', err => {
110
- if (tempPath) fs.unlink(tempPath, () => {})
111
- reject(err)
149
+ fail(err)
112
150
  })
113
151
  req.pipe(busboy)
114
152
  })