fix(media): deriva extensão/content-type do mimetype (corrige arquivos .bin)

Documentos sem nome/extensão eram salvos como .bin mesmo tendo mimetype
conhecido. Agora a extensão vem de: nome original → mimetype (pdf/docx/xlsx/
mp3/...) → .bin só como último recurso. O serve do Wasabi (/api/storage/view)
também passa a mapear content-type para muitos tipos, não só octet-stream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-06-28 06:11:00 +02:00
parent 6d08a5b044
commit 8d35fa1fdf
2 changed files with 59 additions and 9 deletions
@@ -913,8 +913,12 @@ export class MessageHandler {
const originalMimetype = docMsg?.mimetype || this.mimetypeForType(contentType)
let ext: string
if (contentType === 'documentMessage' && originalFileName) {
ext = path.extname(originalFileName).replace('.', '') || 'bin'
if (contentType === 'documentMessage') {
// Ordem: 1) extensão do nome original; 2) derivada do mimetype real do
// documento; 3) só então .bin. Antes ia direto p/ .bin quando o doc não
// tinha nome/extensão — mesmo tendo mimetype conhecido (pdf, docx…).
const fromName = originalFileName ? path.extname(originalFileName).replace('.', '') : ''
ext = fromName || this.extFromMimetype(originalMimetype) || 'bin'
} else {
ext = this.extensionForType(contentType)
}
@@ -1107,6 +1111,45 @@ export class MessageHandler {
return map[contentType] ?? 'bin'
}
/** Deriva a extensão de arquivo a partir do mimetype (documentos variados). */
private extFromMimetype(mimetype?: string | null): string | null {
if (!mimetype) return null
const clean = mimetype.split(';')[0].trim().toLowerCase()
const map: Record<string, string> = {
'application/pdf': 'pdf',
'application/msword': 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'application/vnd.ms-excel': 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
'application/vnd.ms-powerpoint': 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
'application/zip': 'zip',
'application/x-rar-compressed': 'rar',
'application/vnd.rar': 'rar',
'application/x-7z-compressed': '7z',
'application/gzip': 'gz',
'text/plain': 'txt',
'text/csv': 'csv',
'text/html': 'html',
'application/json': 'json',
'application/xml': 'xml',
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/webp': 'webp',
'video/mp4': 'mp4',
'video/3gpp': '3gp',
'video/quicktime': 'mov',
'audio/ogg': 'ogg',
'audio/mpeg': 'mp3',
'audio/mp4': 'm4a',
'audio/amr': 'amr',
'audio/aac': 'aac',
'audio/wav': 'wav',
}
return map[clean] ?? null
}
/** Retorna o mimetype adequado para o tipo de mídia */
private mimetypeForType(contentType: string): string {
const map: Record<string, string> = {
@@ -126,13 +126,20 @@ async function bootstrap() {
const buffer = await storageProvider.getBuffer(filePath)
if (!buffer) { res.status(404).end(); return }
const ext = filePath.split('.').pop()?.toLowerCase()
const mime =
ext === 'webp' ? 'image/webp' :
ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' :
ext === 'png' ? 'image/png' :
ext === 'mp4' ? 'video/mp4' :
ext === 'ogg' ? 'audio/ogg' :
'application/octet-stream'
const MIME_BY_EXT: Record<string, string> = {
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp',
mp4: 'video/mp4', '3gp': 'video/3gpp', mov: 'video/quicktime',
ogg: 'audio/ogg', mp3: 'audio/mpeg', m4a: 'audio/mp4', amr: 'audio/amr', aac: 'audio/aac', wav: 'audio/wav',
pdf: 'application/pdf', doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
zip: 'application/zip', rar: 'application/vnd.rar', '7z': 'application/x-7z-compressed', gz: 'application/gzip',
txt: 'text/plain', csv: 'text/csv', json: 'application/json', xml: 'application/xml', html: 'text/html',
}
const mime = (ext && MIME_BY_EXT[ext]) ?? 'application/octet-stream'
res.setHeader('Content-Type', mime)
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
res.send(buffer)