9.3 KiB
Correção Wasabi Storage — newwhats
Data: 2026-05-07
Investigado e corrigido por: Claude Code
Contexto
O plugin UnifiedStorageProvider estava configurado para enviar mídias do WhatsApp ao Wasabi, porém os arquivos continuavam acumulando localmente em backend/media, chegando a 7.8 GB de mídia que deveria estar na nuvem. A causa raiz foi uma combinação de 4 bugs no código e uma inconsistência de path descoberta na revisão final.
Diagnóstico — 4 bugs + 1 inconsistência corrigidos
Bug 1 — Arquivo local nunca era deletado após upload Wasabi (crítico)
Arquivo: backend/src/modules/whatsapp/handlers/MessageHandler.ts
O fluxo era:
- Arquivo de mídia chegava → salvo em
backend/media/{instanceId}/{msgId}.ext✅ - StorageProvider registrado → upload para Wasabi ✅
- ❌ Arquivo local NUNCA era deletado após upload bem-sucedido
Resultado: disco enchia indefinidamente mesmo com Wasabi funcionando.
Correção: Após upload bem-sucedido (provider !== 'local_fallback'), o arquivo local é removido com fs.unlink() assíncrono.
Bug 2 — mediaPath apontava para arquivo local inexistente (crítico)
Arquivo: backend/src/modules/whatsapp/handlers/MessageHandler.ts
Mesmo após o upload para Wasabi, o campo mediaPath no banco continuava sendo salvo com o caminho local completo (/home/deploy/.../media/...), que após o Bug 1 corrigido deixa de existir no disco.
Correção: Quando uploadedToRemote = true, mediaPath é salvo como null no banco. O acesso à mídia passa a ser exclusivamente pelo mediaUrl que aponta para o Wasabi.
Bug 3 — Rotas HTTP duplicadas ao reativar plugin via admin (médio)
Arquivo: plugins/UnifiedStorageProvider/index.ts
O pluginRegistry.reactivate() chama activate() novamente a cada salvamento de config. O activate() registrava as rotas Express (/api/storage/view/*, /api/storage/wasabi/list-buckets, etc.) a cada chamada, acumulando handlers duplicados em memória.
Correção: Adicionada flag private routesRegistered = false. As rotas Express são registradas apenas na primeira chamada de activate(). Reativações subsequentes reinicializam apenas o S3 client e o registro do StorageProvider.
Bug 4 — Deleção de instância não limpava mídias no Wasabi (funcionalidade ausente)
Arquivo: backend/src/modules/whatsapp/whatsapp.routes.ts
Ao deletar uma instância pelo admin, o fluxo só desconectava o socket Baileys, removia os arquivos de sessão e apagava o registro do banco. As mídias no Wasabi (e no fallback local) ficavam permanentemente órfãs.
Implementação:
- Adicionado
deletePrefix(prefix: string): Promise<{ deleted: number }>à interfaceStorageProviderInterfaceemtypes.ts - Exposto
deletePrefix()noStorageProviderRegistryemStorageProvider.ts(silencioso se provider não registrado) - Implementado no plugin com
ListObjectsV2Commandpaginado em batches de 1000 +DeleteObjectsCommand, limpando também o fallback local - Chamado em background na rota
DELETE /:idapósrepo.delete(id), sem bloquear a resposta
Inconsistência 5 — Path incorreto no deletePrefix (crítico)
Arquivo: backend/src/modules/whatsapp/whatsapp.routes.ts
O plugin grava arquivos no Wasabi com o path:
whatsapp/{tenantId}/{instanceId}/{fileName}
A implementação inicial do deletePrefix usava:
whatsapp/{instanceId}/ ← errado, nunca encontraria os arquivos
Correção: Prefix corrigido para whatsapp/${tenantId}/${id}/, que bate exatamente com a estrutura gravada pelo plugin.
Arquivos modificados
| Arquivo | Mudança |
|---|---|
backend/src/core/types.ts |
Adicionado deletePrefix() à interface StorageProviderInterface |
backend/src/core/StorageProvider.ts |
Exposto deletePrefix() no registry com fallback silencioso |
backend/src/modules/whatsapp/handlers/MessageHandler.ts |
fs.unlink() após upload + mediaPath = null quando remoto |
backend/src/modules/whatsapp/whatsapp.routes.ts |
deletePrefix("whatsapp/{tenantId}/{id}/") ao deletar instância |
plugins/UnifiedStorageProvider/index.ts |
Flag routesRegistered + import DeleteObjectsCommand + implementação de deletePrefix() |
Configuração atual (verificada)
// DragonflyDB — plugin:config:UnifiedStorageProvider
{
"wasabiAccessKey": "OFLCYUUZRYJFKHLB81QC",
"wasabiSecretKey": "R4GI2s7W...",
"wasabiBucket": "whatsbasemidia",
"wasabiRegion": "us-east-1"
}
Endpoint padrão: https://s3.wasabisys.com
Situação das mídias antigas (pós-investigação)
| Situação | Quantidade | Ação |
|---|---|---|
| Migradas para Wasabi pelo script | 231 arquivos | ✅ no Wasabi (path: whatsapp/{instanceId}/) |
| Deletadas do servidor após migração | 231 arquivos | ✅ removidas de backend/media |
| Banco atualizado (mediaUrl → Wasabi) | 231 registros | ✅ mediaPath = null, mediaUrl = whatsapp/... |
Ainda locais em backend/media |
~1.430 registros | Serão refeitos ao reconectar sessões |
| Novas mídias (pós-correção) | todas | ✅ vão direto para Wasabi e local é deletado |
⚠️ Os 231 arquivos migrados pelo script têm path
whatsapp/{instanceId}/(sem tenantId) pois foram lidos direto do disco. O plugin novo grava comowhatsapp/{tenantId}/{instanceId}/. Isso não quebra o funcionamento mas odeletePrefixautomático não os apagará quando a instância for deletada — precisarão ser removidos manualmente do bucket se necessário.
Script de migração criado
Arquivo: migrate-media-wasabi.mjs (raiz do projeto)
Script Node.js criado para migrar os arquivos acumulados em backend/media para o Wasabi. Suporta --dry-run para simulação sem alterações.
O que o script faz por arquivo:
- Lê
messagesdo banco ondemediaUrl LIKE '/media/%' - Verifica se o arquivo já existe no Wasabi (
HeadObjectCommand) — pula upload se sim - Faz upload para
whatsapp/{instanceId}/{fileName}no bucketwhatsbasemidia - Atualiza o banco:
mediaUrl = 'whatsapp/...',mediaPath = NULL - Remove o arquivo local com
fs.unlinkSync() - Ao final, remove diretórios vazios de
backend/media
Execução:
# Simulação (sem alterar nada):
node migrate-media-wasabi.mjs --dry-run
# Migração real:
node migrate-media-wasabi.mjs
Status da execução em 2026-05-07:
- Migração iniciada e interrompida manualmente aos 231/1.661 arquivos
- Motivo da parada: decisão de deletar sessões manualmente e reconectar para que as mídias sejam baixadas novamente do WhatsApp já com o Wasabi funcionando
Backup gerado antes das operações
Arquivo: /home/deploy/projetos/newwhats_export/newwhats_completo.tar.gz — 7.7 GB
Dump do banco: /home/deploy/projetos/newwhats_export/newwhats_dump.sql — 18 MB
O tar.gz contém o projeto completo (incluindo backend/media com todas as mídias locais no estado anterior à migração) e o dump PostgreSQL. Gerado em 2026-05-07 como snapshot de segurança antes de qualquer alteração.
Fluxo completo após as correções
Upload de nova mídia:
WhatsApp recebe mídia
→ salva buffer temporariamente em backend/media/{instanceId}/{msgId}.ext
→ storageProvider.uploadFile() → Wasabi: whatsapp/{tenantId}/{instanceId}/{fileName}
→ fs.unlink(localFile) ← arquivo local removido
→ banco: mediaPath = null, mediaUrl = whatsapp/{tenantId}/{instanceId}/{fileName}
→ frontend acessa via /api/storage/view/whatsapp/... (proxy → Wasabi)
Se Wasabi falhar:
→ arquivo local mantido em backend/media
→ banco: mediaPath = /caminho/local, mediaUrl = /media/{instanceId}/{fileName}
→ SyncWorker tenta reenviar a cada 2 minutos
→ quando enviado: arquivo local removido automaticamente
Ao deletar instância:
DELETE /api/whatsapp/{id}
→ manager.disconnect() → desconecta Baileys
→ repo.delete() → remove do banco (cascade: chats, mensagens, contatos)
→ responde 200 ← não bloqueia
→ [background] storageProvider.deletePrefix("whatsapp/{tenantId}/{id}/")
→ ListObjectsV2 paginado (batches de 1000)
→ DeleteObjects em cada batch
→ Remove fallback local se existir
→ Loga total removidos
---
## Status da Implementação Física em Produção (VPS 1) — 2026-05-07
Todas as correções descritas neste documento foram fisicamente implementadas, compiladas, testadas e implantadas com sucesso na VPS 1 de Produção.
### Ações Executadas:
1. **Ajuste de Interfaces e Código Core:** Modificados `types.ts`, `StorageProvider.ts`, `MessageHandler.ts`, e `whatsapp.routes.ts`.
2. **Implementação de Limpeza Automática:** `UnifiedStorageProvider/index.ts` agora conta com `deletePrefix` S3 e deleção local robusta.
3. **Correção de Arquitetura de Container (Crítica):** Foi detectado que nenhum plugin era carregado em produção por falta de mapeamento. Montamos o volume de plugins e criamos os links simbólicos necessários (`/plugins`, `/app/backend`, `/app/src`) no Dockerfile para que os imports relativos funcionem na perfeição.
4. **Re-deploy Completo:** O container `newwhats-backend` foi totalmente rebuildado e reiniciado com `docker compose up -d --build`.
**Status Final:** ✅ **CORRIGIDO E IMPLEMENTADO EM PRODUÇÃO**