docs(newwhats): runbook de migração de mídia local → Wasabi + limpeza de disco
- novo: wasabi-migracao-e-limpeza.md (fluxo executado 04/jul, v1.0.70) - wasabi-midias.md: ver-também cruzado Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
> Estado atual + desenho do que falta. Storage = **Wasabi (S3-compatível)** via
|
> Estado atual + desenho do que falta. Storage = **Wasabi (S3-compatível)** via
|
||||||
> `StorageProvider` (`backend/src/core/StorageProvider.ts`). Última revisão: 28/jun/2026.
|
> `StorageProvider` (`backend/src/core/StorageProvider.ts`). Última revisão: 28/jun/2026.
|
||||||
|
>
|
||||||
|
> **Ver também:** `wasabi-migracao-e-limpeza.md` — runbook de migração da mídia local
|
||||||
|
> `/media/` → Wasabi + esvaziamento do disco (executado 04/jul/2026, v1.0.70).
|
||||||
|
|
||||||
## Como funciona hoje
|
## Como funciona hoje
|
||||||
- **Upload** (`uploadFile`): fire-and-forget após receber a mídia/avatar; grava no
|
- **Upload** (`uploadFile`): fire-and-forget após receber a mídia/avatar; grava no
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# newwhats.clube67.com — Migração de mídia local → Wasabi & limpeza de disco
|
||||||
|
|
||||||
|
> Runbook **executado no DEV em 04/jul/2026 (release v1.0.70)** para tirar TODA a mídia
|
||||||
|
> do disco do servidor e deixar só no Wasabi. Repetível em produção com os alvos de PROD.
|
||||||
|
> Complementa `wasabi-midias.md` (design de index + delete em lote); aqui é o **fluxo de
|
||||||
|
> migração one-shot + limpeza**, a feature de **thumbnails** e o **guard de recuperabilidade**.
|
||||||
|
|
||||||
|
## Contexto: onde a mídia vive
|
||||||
|
|
||||||
|
Campo `mediaUrl` na tabela `messages` (Postgres `newwhats_dev` / prod equivalente; colunas
|
||||||
|
camelCase, model Prisma `@@map("messages")`) tem 3 estados:
|
||||||
|
|
||||||
|
| `mediaUrl` | Significado | Arquivo em disco? |
|
||||||
|
|---|---|---|
|
||||||
|
| `whatsapp/<tenant>/<instance>/<ts>-<nome>` | já no **Wasabi** (servido remoto) | não |
|
||||||
|
| `/media/<instanceId>/<file>` | **LOCAL** — servido do disco | sim (`/app/media/...`) |
|
||||||
|
| `NULL` | lazy — re-baixável do WhatsApp via `mediaPayload` | não |
|
||||||
|
|
||||||
|
Mídia local mora em `newwhats.dev/data/media` (montado como `/app/media` no container
|
||||||
|
`newwhats-backend-dev`). **`newwhats.dev` é a cópia em execução (git-ignored); `newwhats.local`
|
||||||
|
é a fonte versionada.** O container monta `newwhats.dev/plugins`.
|
||||||
|
|
||||||
|
## Config do Wasabi (importante: NÃO está em env)
|
||||||
|
|
||||||
|
Vem do **Dragonfly**, chave `plugin:config:UnifiedStorageProvider` (DB `/1` em
|
||||||
|
`redis://…@10.99.0.3:6379/1`; a senha está na `DRAGONFLY_URL` do container). Ler:
|
||||||
|
|
||||||
|
```
|
||||||
|
redis-cli -h 10.99.0.3 -p 6379 -a <senha> -n 1 GET plugin:config:UnifiedStorageProvider
|
||||||
|
```
|
||||||
|
|
||||||
|
Valores DEV: bucket `whatsbasemidia`, endpoint `https://s3.wasabisys.com`, region `us-east-1`.
|
||||||
|
Cliente S3 = `forcePathStyle:true`; upload com `ACL:'public-read'`.
|
||||||
|
|
||||||
|
## Convenção de key (replicar EXATAMENTE)
|
||||||
|
|
||||||
|
Plugin ativo `newwhats.dev/plugins/UnifiedStorageProvider/index.js` (~L330):
|
||||||
|
|
||||||
|
```
|
||||||
|
whatsapp/<tenantId>/<instanceId>/<Date.now()>-<nomeSanitizado>
|
||||||
|
```
|
||||||
|
|
||||||
|
As mídias já no Wasabi usam isso — se a key divergir, o serving quebra.
|
||||||
|
|
||||||
|
## Passo a passo da migração
|
||||||
|
|
||||||
|
Rodar **DENTRO do container** `newwhats-backend-dev` (tem `@aws-sdk/client-s3`, `pg`, os
|
||||||
|
arquivos em `/app/media` e `DATABASE_URL`): `export NODE_PATH=/app/node_modules; cd /app`.
|
||||||
|
|
||||||
|
Para cada linha `WHERE "mediaUrl" LIKE '/media/%'`:
|
||||||
|
1. ler o arquivo local (`/app/media/` + `mediaUrl` sem o prefixo `/media/`);
|
||||||
|
2. `PutObject` no Wasabi com a key acima, `ContentType` do mime, `ACL:'public-read'`;
|
||||||
|
3. **`HeadObject` conferindo `ContentLength == tamanho local`** (rede de segurança);
|
||||||
|
4. só então `UPDATE messages SET "mediaUrl" = <key>`;
|
||||||
|
5. `unlinkSync` do arquivo local.
|
||||||
|
|
||||||
|
Idempotente e resumível (ao migrar, o `mediaUrl` deixa de casar `/media/%`). **Rodar
|
||||||
|
`DRY_RUN=1` primeiro** (verifica existência/tamanho, sem enviar/apagar). Script de
|
||||||
|
referência: `migrate_media.js`.
|
||||||
|
|
||||||
|
### Refs mortas
|
||||||
|
`/media/…` **sem arquivo E sem `mediaPayload`** = referência quebrada (no DEV eram 257).
|
||||||
|
`UPDATE messages SET "mediaUrl" = NULL` → aí o guard `media_recoverable=false` exibe
|
||||||
|
"indisponível" de forma limpa (em vez de tentar carregar path quebrado → 404).
|
||||||
|
|
||||||
|
### Órfãos e esvaziamento final
|
||||||
|
Após migrar tudo, nenhum `mediaUrl` referencia arquivo local → qualquer arquivo que
|
||||||
|
sobrar em `data/media` é órfão. Confirmar `erros=0` no resumo e então esvaziar
|
||||||
|
(`find data/media -mindepth 1 -delete`). No DEV liberou ~2,1 GB.
|
||||||
|
|
||||||
|
## Feature acoplada (mesma release)
|
||||||
|
|
||||||
|
- **Thumbnails**: no re-download, além de subir a mídia, gera `sharp → 400px → webp q60`
|
||||||
|
e grava `<mediaUrl>.thumb.webp` no Wasabi. O `MessageBubble` pede a thumb na prévia e
|
||||||
|
cai pro full via `onError`. (commit `2c8eb41` / submódulo `09edeb9`)
|
||||||
|
- **`mediaRecoverable`**: `GET /inbox/:chatId/messages` devolve `Boolean(mediaUrl || mediaPath
|
||||||
|
|| mediaPayload)`; o frontend mapeia p/ `media_recoverable` e o guard evita o `400` de
|
||||||
|
`/media/:id/download` em mídia legada.
|
||||||
|
|
||||||
|
## Resultado no DEV (04/jul/2026)
|
||||||
|
|
||||||
|
- 659 mídias migradas (~2,0 GB), 0 erros; 257 refs mortas → NULL; disco: 2,1 GB → 0.
|
||||||
|
- Banco final: 2.231 no Wasabi, 26.793 lazy (NULL+payload), 0 refs `/media/`.
|
||||||
|
|
||||||
|
## Cuidados
|
||||||
|
|
||||||
|
- **Sempre** `HeadObject` antes de apagar o local; nunca apagar sem upload confirmado.
|
||||||
|
- O **"janitor"** do plugin (`setInterval` 120s) só varre `localFallbackDir`/`uploads` e
|
||||||
|
**não atualiza o banco** — não serve para migrar a mídia do WhatsApp.
|
||||||
|
- Limpar temporários com credenciais depois (`/tmp/wasabi.env`, creds do Dragonfly).
|
||||||
|
- Testar sempre no `dev.newwhats` antes; **pedir aprovação antes de rodar em produção**.
|
||||||
|
|
||||||
|
## Para PRODUÇÃO
|
||||||
|
|
||||||
|
O newwhats de produção **não roda na VPS4**. Aplicar o mesmo fluxo contra o diretório de
|
||||||
|
mídia, o banco e a config Dragonfly **de produção**. Ver `ambiente-dev.md` e
|
||||||
|
`infra/maquinas-e-rede.md`.
|
||||||
Reference in New Issue
Block a user