- Fix multipart upload parsing to use Buffer operations instead of latin1 string conversion, preserving multi-byte characters in filenames (#72) - Support RFC 5987 filename* encoding for cross-platform compatibility - Fix in-page update by running npm install directly instead of CLI command that kills the server process before response is sent (#71) - Add no-cache header to version check to avoid stale latest version - Change version check interval from 4 hours to 1 hour Closes #72, Closes #71 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,31 +22,60 @@ uploadRoutes.post('/upload', async (ctx) => {
|
||||
|
||||
await mkdir(config.uploadDir, { recursive: true })
|
||||
|
||||
// Read raw body
|
||||
// Read raw body as Buffer
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of ctx.req) chunks.push(chunk)
|
||||
const body = Buffer.concat(chunks).toString('latin1')
|
||||
const parts = body.split(boundary).slice(1, -1)
|
||||
const raw = Buffer.concat(chunks)
|
||||
const boundaryBuf = Buffer.from(boundary)
|
||||
const parts = splitMultipart(raw, boundaryBuf)
|
||||
|
||||
const results: { name: string; path: string }[] = []
|
||||
|
||||
for (const part of parts) {
|
||||
const headerEnd = part.indexOf('\r\n\r\n')
|
||||
const headerEnd = part.indexOf(Buffer.from('\r\n\r\n'))
|
||||
if (headerEnd === -1) continue
|
||||
const header = part.substring(0, headerEnd)
|
||||
const data = part.substring(headerEnd + 4, part.length - 2)
|
||||
const headerBuf = part.subarray(0, headerEnd)
|
||||
const header = headerBuf.toString('utf-8')
|
||||
const data = part.subarray(headerEnd + 4, part.length - 2)
|
||||
|
||||
const filenameMatch = header.match(/filename="([^"]+)"/)
|
||||
if (!filenameMatch) continue
|
||||
// Try RFC 5987 filename* first, fall back to filename
|
||||
let filename = ''
|
||||
const filenameStarMatch = header.match(/filename\*=UTF-8''(.+)/i)
|
||||
if (filenameStarMatch) {
|
||||
filename = decodeURIComponent(filenameStarMatch[1])
|
||||
} else {
|
||||
const filenameMatch = header.match(/filename="([^"]+)"/)
|
||||
if (!filenameMatch) continue
|
||||
filename = filenameMatch[1]
|
||||
}
|
||||
|
||||
const filename = filenameMatch[1]
|
||||
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
|
||||
const savedName = randomBytes(8).toString('hex') + ext
|
||||
const savedPath = `${config.uploadDir}/${savedName}`
|
||||
|
||||
await writeFile(savedPath, Buffer.from(data, 'binary'))
|
||||
await writeFile(savedPath, data)
|
||||
results.push({ name: filename, path: savedPath })
|
||||
}
|
||||
|
||||
ctx.body = { files: results }
|
||||
})
|
||||
|
||||
/**
|
||||
* Split a multipart Buffer by boundary, returning part Buffers.
|
||||
* Avoids string decoding so multi-byte characters (e.g. Chinese filenames) are preserved.
|
||||
*/
|
||||
function splitMultipart(raw: Buffer, boundary: Buffer): Buffer[] {
|
||||
const parts: Buffer[] = []
|
||||
let start = 0
|
||||
while (true) {
|
||||
const idx = raw.indexOf(boundary, start)
|
||||
if (idx === -1) break
|
||||
if (start > 0) {
|
||||
// Skip the \r\n after boundary
|
||||
const partStart = start + 2
|
||||
parts.push(raw.subarray(partStart, idx))
|
||||
}
|
||||
start = idx + boundary.length
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user