Files
VPS 4 Builder 6b5ebd48ca 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>
2026-07-04 19:18:39 +02:00

4.6 KiB

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.