From 4ab3af503be0caf40d8cb451e44bcaa2605030d1 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 21 Jul 2026 09:47:44 -0300 Subject: [PATCH] =?UTF-8?q?FEAT:=20Adiciona=20camada=20de=20storage=20de?= =?UTF-8?q?=20m=C3=ADdia=20(LocalDiskAdapter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MediaStorageService com interface upload/getUrl/delete - LocalDiskAdapter salva em /uploads/media/ com UUID + extensão por mimetype - Cria o diretório automaticamente no boot - BASE_URL adicionado ao .env.example - MediaStorageService e LocalDiskAdapter registrados no WhatsappModule --- .env.example | 1 + .../media/adapters/local-disk.adapter.ts | 48 +++++++++++++++++++ .../whatsapp/media/media-storage.service.ts | 20 ++++++++ src/modules/whatsapp/whatsapp.module.ts | 4 ++ 4 files changed, 73 insertions(+) create mode 100644 src/modules/whatsapp/media/adapters/local-disk.adapter.ts create mode 100644 src/modules/whatsapp/media/media-storage.service.ts diff --git a/.env.example b/.env.example index 51c8018..fa30729 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ DB_NAME=omnichannel # HTTP/JWT FRONTEND_URL=http://localhost:3000 +BASE_URL=http://localhost:3001 JWT_SECRET=change-this-long-random-secret JWT_EXPIRES_IN=8h diff --git a/src/modules/whatsapp/media/adapters/local-disk.adapter.ts b/src/modules/whatsapp/media/adapters/local-disk.adapter.ts new file mode 100644 index 0000000..87e2265 --- /dev/null +++ b/src/modules/whatsapp/media/adapters/local-disk.adapter.ts @@ -0,0 +1,48 @@ +import { Injectable, Logger } from '@nestjs/common'; +import * as fs from 'fs'; +import * as path from 'path'; +import { randomUUID } from 'crypto'; + +const UPLOAD_DIR = path.resolve(process.cwd(), 'uploads', 'media'); + +const MIME_TO_EXT: Record = { + 'image/jpeg': '.jpg', + 'image/png': '.png', + 'image/webp': '.webp', + 'audio/ogg': '.ogg', + 'audio/mpeg': '.mp3', + 'audio/mp4': '.m4a', + 'audio/amr': '.amr', + 'video/mp4': '.mp4', + 'video/3gpp': '.3gp', + 'application/pdf': '.pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', +}; + +@Injectable() +export class LocalDiskAdapter { + private readonly logger = new Logger(LocalDiskAdapter.name); + + constructor() { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + async upload(buffer: Buffer, mimeType: string): Promise { + const ext = MIME_TO_EXT[mimeType] ?? '.bin'; + const filename = `${randomUUID()}${ext}`; + const filepath = path.join(UPLOAD_DIR, filename); + fs.writeFileSync(filepath, buffer); + this.logger.log(`Mídia salva: ${filepath}`); + return `/uploads/media/${filename}`; + } + + delete(relativePath: string): void { + try { + fs.unlinkSync(path.join(process.cwd(), relativePath)); + } catch { + this.logger.warn(`Falha ao deletar mídia: ${relativePath}`); + } + } +} diff --git a/src/modules/whatsapp/media/media-storage.service.ts b/src/modules/whatsapp/media/media-storage.service.ts new file mode 100644 index 0000000..a24891b --- /dev/null +++ b/src/modules/whatsapp/media/media-storage.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { LocalDiskAdapter } from './adapters/local-disk.adapter'; + +@Injectable() +export class MediaStorageService { + constructor(private readonly disk: LocalDiskAdapter) {} + + async upload(buffer: Buffer, mimeType: string): Promise { + return this.disk.upload(buffer, mimeType); + } + + getUrl(relativePath: string): string { + const base = (process.env.BASE_URL ?? 'http://localhost:3001').replace(/\/$/, ''); + return `${base}${relativePath}`; + } + + delete(relativePath: string): void { + this.disk.delete(relativePath); + } +} diff --git a/src/modules/whatsapp/whatsapp.module.ts b/src/modules/whatsapp/whatsapp.module.ts index 90ad9e3..a5dab5a 100644 --- a/src/modules/whatsapp/whatsapp.module.ts +++ b/src/modules/whatsapp/whatsapp.module.ts @@ -11,6 +11,8 @@ import { WhatsappTemplateService } from './whatsapp-template.service'; import { MensagensRepository } from './repositories/mensagens.repository'; import { WhatsappConfigRepository } from './repositories/whatsapp-config.repository'; import { WhatsappConfigService } from './whatsapp-config.service'; +import { MediaStorageService } from './media/media-storage.service'; +import { LocalDiskAdapter } from './media/adapters/local-disk.adapter'; @Module({ imports: [AttendanceModule, AuthModule, ContactsModule], @@ -22,6 +24,8 @@ import { WhatsappConfigService } from './whatsapp-config.service'; MensagensRepository, WhatsappConfigRepository, WhatsappConfigService, + MediaStorageService, + LocalDiskAdapter, ], controllers: [WhatsappController, WhatsappAdminController], exports: [WhatsappService],