From 8d35fa1fdf8b23743f47c99e788af794bbb782ee Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Sun, 28 Jun 2026 06:11:00 +0200 Subject: [PATCH] =?UTF-8?q?fix(media):=20deriva=20extens=C3=A3o/content-ty?= =?UTF-8?q?pe=20do=20mimetype=20(corrige=20arquivos=20.bin)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../whatsapp/handlers/MessageHandler.ts | 47 ++++++++++++++++++- .../newwhats.local/backend/src/server.ts | 21 ++++++--- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts index 73ef50a..d4ee67e 100644 --- a/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts +++ b/newwhats.clube67.com/newwhats.local/backend/src/modules/whatsapp/handlers/MessageHandler.ts @@ -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 = { + '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 = { diff --git a/newwhats.clube67.com/newwhats.local/backend/src/server.ts b/newwhats.clube67.com/newwhats.local/backend/src/server.ts index 33fec6b..f703276 100644 --- a/newwhats.clube67.com/newwhats.local/backend/src/server.ts +++ b/newwhats.clube67.com/newwhats.local/backend/src/server.ts @@ -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 = { + 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)