feat: add cloud backup upload and universal restore script

This commit is contained in:
Erik
2025-11-29 12:44:47 -03:00
parent 1600cc8267
commit 932caf1b6c
4 changed files with 587 additions and 0 deletions

179
docs/BACKUP_CLOUD.md Normal file
View File

@@ -0,0 +1,179 @@
# Sistema de Backup em Cloud - Occto Engenharia
Sistema completo de backup e restore com upload automático para cloud (MinIO/S3).
## 🎯 Funcionalidades
- ✅ Criar backups locais (PostgreSQL + MinIO)
-**Upload automático** para cloud (MinIO/S3)
- ✅ Download de backups
- ✅ Restore local com um clique
-**Script universal** de restore para qualquer servidor
## 📱 Interface Admin
Acesse `https://seu-dominio.com/admin/configuracoes` → Aba "Backup"
### Botões disponíveis:
- ☁️ **Upload** - Enviar backup para cloud
- 🔄 **Restaurar** - Restaurar backup no banco atual
- ⬇️ **Baixar** - Download do arquivo `.tar.gz`
- 🗑️ **Deletar** - Remover backup local
## 🚀 Restauração Rápida (Curly Magic)
### Opção 1: Via URL do Cloud (Recomendado)
```bash
bash <(curl -s https://seu-dominio.com/restore-from-cloud.sh) \
--backup-url "http://seu-minio:9000/backups/backup-2025-11-29.tar.gz" \
--postgres-password "sua_senha_postgres" \
--minio-endpoint "seu-minio"
```
### Opção 2: Arquivo Local
```bash
bash scripts/restore-from-cloud.sh \
--backup-file "backup-2025-11-29.tar.gz" \
--postgres-password "sua_senha_postgres"
```
### Opção 3: Com Docker Compose
```bash
# 1. Coloque o backup.tar.gz no projeto
# 2. Execute o script
bash scripts/restore-from-cloud.sh \
--backup-file "backup-2025-11-29.tar.gz" \
--postgres-password "sua_senha" \
--postgres-host "postgres" \
--postgres-db "occto_db"
```
## 📋 Parâmetros do Script
| Parâmetro | Padrão | Descrição |
|-----------|--------|-----------|
| `--backup-url` | - | URL do backup (HTTP, S3, etc) |
| `--backup-file` | - | Nome do arquivo (obrigatório) |
| `--postgres-password` | `adminpassword` | Senha do PostgreSQL |
| `--postgres-db` | `occto_db` | Nome do banco |
| `--postgres-host` | `postgres` | Host do PostgreSQL |
| `--postgres-user` | `admin` | Usuário do PostgreSQL |
| `--minio-endpoint` | `minio` | Host do MinIO |
## 🔧 Integração com MinIO
O sistema automaticamente:
1. Cria bucket `backups` se não existir
2. Faz upload do `.tar.gz` para `backups/`
3. Gera URL acessível do arquivo
### Configurar MinIO (Docker)
```yaml
# docker-compose.yml
minio:
image: minio/minio:latest
environment:
MINIO_ROOT_USER: admin
MINIO_ROOT_PASSWORD: adminpassword
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/minio_data
command: server /minio_data --console-address ":9001"
```
## 🛡️ Segurança
- ⚠️ O script remove o banco antigo antes de restaurar
- ⚠️ Pede confirmação antes de restaurar
- ✅ Valida arquivo antes de extrair
- ✅ Limpa arquivos temporários automaticamente
## 📊 Exemplo de Uso Completo
```bash
# 1. Ir para o projeto
cd /seu/projeto/occto
# 2. Criar backup via admin UI (opcional)
# Ou fazer manual:
./scripts/create-backup.sh
# 3. Upload para cloud (via UI ou manual)
curl -X POST http://localhost:3000/api/backup/upload \
-H "Content-Type: application/json" \
-d '{"filename": "backup-2025-11-29.tar.gz"}'
# 4. Em outro servidor, restaurar:
bash <(curl -s http://localhost:3000/restore-from-cloud.sh) \
--backup-url "http://seu-minio:9000/backups/backup-2025-11-29.tar.gz" \
--postgres-password "nova_senha"
# 5. Reiniciar aplicação
docker-compose restart
```
## 🐛 Troubleshooting
### "Arquivo não encontrado"
```bash
# Verificar arquivos disponíveis
ls -lah .backups/
```
### "Erro ao conectar PostgreSQL"
```bash
# Testar conexão
PGPASSWORD="senha" psql -h postgres -U admin -d occto_db -c "SELECT 1"
```
### "MinIO connection refused"
```bash
# Verificar se MinIO está rodando
docker ps | grep minio
```
### Restaurar manualmente
```bash
# Se o script falhar, fazer passo a passo
tar -xzf backup-2025-11-29.tar.gz
PGPASSWORD="senha" psql -h postgres -U admin -d occto_db < database.sql
docker cp minio-data/. seu-container-minio:/data/
docker restart seu-container-minio
```
## 📝 Estrutura do Backup
```
backup-2025-11-29.tar.gz
├── database.sql # Dump completo do PostgreSQL
└── minio-data/
├── [todos os buckets e arquivos]
└── [estrutura original do MinIO]
```
## 🔄 Automation (Cron)
Para fazer backups automáticos a cada dia:
```bash
# Adicionar ao crontab
0 2 * * * cd /seu/projeto && ./scripts/create-backup.sh && curl -X POST http://localhost:3000/api/backup/upload -d '{"filename":"backup-$(date +\%Y-\%m-\%d).tar.gz"}'
```
## 📞 Suporte
Para dúvidas ou problemas, verifique:
1. Logs: `docker logs seu-container-next`
2. Espaço em disco: `df -h`
3. Permissões: `ls -la .backups/`
---
**Última atualização:** 29 de Novembro de 2025

View File

@@ -0,0 +1,131 @@
import { NextRequest, NextResponse } from 'next/server';
import * as fs from 'fs';
import * as path from 'path';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
// Configuração MinIO/S3
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'minio';
const MINIO_PORT = process.env.MINIO_PORT || '9000';
const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || 'admin';
const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY || 'adminpassword';
const MINIO_USE_SSL = process.env.MINIO_USE_SSL === 'true';
const MINIO_BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'backups';
// Diretório de backups locais
const BACKUP_DIR = path.join(process.cwd(), '.backups');
// Inicializar cliente S3 (MinIO é compatível com S3)
const s3Client = new S3Client({
region: 'us-east-1',
endpoint: `http${MINIO_USE_SSL ? 's' : ''}://${MINIO_ENDPOINT}:${MINIO_PORT}`,
credentials: {
accessKeyId: MINIO_ACCESS_KEY,
secretAccessKey: MINIO_SECRET_KEY,
},
forcePathStyle: true,
});
/**
* POST /api/backup/upload
* Faz upload de um backup local para MinIO/S3
*/
export async function POST(request: NextRequest) {
try {
const { filename } = await request.json();
if (!filename) {
return NextResponse.json(
{ success: false, error: 'Filename é obrigatório' },
{ status: 400 }
);
}
const backupPath = path.join(BACKUP_DIR, filename);
// Validar se arquivo existe
if (!fs.existsSync(backupPath)) {
return NextResponse.json(
{ success: false, error: 'Arquivo de backup não encontrado' },
{ status: 404 }
);
}
// Ler arquivo
const fileContent = fs.readFileSync(backupPath);
const fileSize = fs.statSync(backupPath).size;
console.log(`[BACKUP UPLOAD] Iniciando upload de ${filename} (${fileSize} bytes)...`);
// Upload para MinIO
const command = new PutObjectCommand({
Bucket: MINIO_BUCKET_NAME,
Key: `backups/${filename}`,
Body: fileContent,
ContentType: 'application/gzip',
ContentLength: fileSize,
});
await s3Client.send(command);
console.log(`[BACKUP UPLOAD] Upload concluído: ${filename}`);
return NextResponse.json({
success: true,
message: 'Backup enviado para cloud com sucesso',
filename,
size: fileSize,
url: `${MINIO_USE_SSL ? 'https' : 'http'}://${MINIO_ENDPOINT}:${MINIO_PORT}/${MINIO_BUCKET_NAME}/backups/${filename}`,
});
} catch (error) {
console.error('[BACKUP UPLOAD] Erro:', error);
return NextResponse.json(
{
success: false,
error: (error as Error).message,
},
{ status: 500 }
);
}
}
/**
* GET /api/backup/upload/list
* Lista backups na cloud
*/
export async function GET(request: NextRequest) {
try {
const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
const command = new ListObjectsV2Command({
Bucket: MINIO_BUCKET_NAME,
Prefix: 'backups/',
});
const response = await s3Client.send(command);
const backups = (response.Contents || [])
.map((obj) => ({
key: obj.Key,
filename: obj.Key?.replace('backups/', '') || '',
size: obj.Size || 0,
lastModified: obj.LastModified?.toISOString() || '',
}))
.filter((b) => b.filename); // Remover pasta vazia
return NextResponse.json({
success: true,
backups,
count: backups.length,
});
} catch (error) {
console.error('[BACKUP LIST] Erro:', error);
return NextResponse.json(
{
success: false,
error: (error as Error).message,
backups: [],
count: 0,
},
{ status: 500 }
);
}
}

View File

@@ -151,6 +151,32 @@ export function BackupManager() {
}
};
const uploadBackupToCloud = async (filename: string) => {
try {
setRestoreLoading(filename);
const response = await fetch('/api/backup/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename })
});
const data = await response.json();
if (response.ok) {
success('Backup enviado para cloud com sucesso!');
} else {
showError(data.error || 'Erro ao enviar backup');
}
} catch (err) {
showError('Erro ao enviar backup: ' + (err as Error).message);
} finally {
setRestoreLoading(null);
}
};
const formatSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
@@ -247,6 +273,18 @@ export function BackupManager() {
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => uploadBackupToCloud(backup.filename)}
disabled={restoreLoading === backup.filename}
title="Enviar para cloud"
className="p-2 hover:bg-purple-100 dark:hover:bg-purple-900/20 rounded-lg transition-colors text-purple-600 dark:text-purple-400 disabled:opacity-50 disabled:cursor-not-allowed"
>
{restoreLoading === backup.filename ? (
<i className="ri-loader-4-line animate-spin text-xl"></i>
) : (
<i className="ri-cloud-upload-line text-xl"></i>
)}
</button>
<button
onClick={() => restoreBackup(backup.filename)}
disabled={restoreLoading === backup.filename}

View File

@@ -0,0 +1,239 @@
#!/bin/bash
#######################################################
# Script de Restauração de Backup do Occto Engenharia
#
# Uso:
# bash restore-from-cloud.sh \
# --backup-url "http://minio:9000/backups/backup-2025-11-29.tar.gz" \
# --backup-file "backup-2025-11-29.tar.gz" \
# --postgres-password "sua_senha" \
# --postgres-db "occto_db" \
# --postgres-host "postgres"
#######################################################
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Variáveis padrão
BACKUP_URL=""
BACKUP_FILE=""
POSTGRES_PASSWORD="adminpassword"
POSTGRES_USER="admin"
POSTGRES_DB="occto_db"
POSTGRES_HOST="postgres"
POSTGRES_PORT="5432"
MINIO_ENDPOINT="minio"
MINIO_PORT="9000"
MINIO_ACCESS_KEY="admin"
MINIO_SECRET_KEY="adminpassword"
PROJECT_DIR=$(pwd)
TEMP_DIR="/tmp/occto-restore-$$"
# Parsing de argumentos
while [[ $# -gt 0 ]]; do
case $1 in
--backup-url)
BACKUP_URL="$2"
shift 2
;;
--backup-file)
BACKUP_FILE="$2"
shift 2
;;
--postgres-password)
POSTGRES_PASSWORD="$2"
shift 2
;;
--postgres-db)
POSTGRES_DB="$2"
shift 2
;;
--postgres-host)
POSTGRES_HOST="$2"
shift 2
;;
--postgres-user)
POSTGRES_USER="$2"
shift 2
;;
--minio-endpoint)
MINIO_ENDPOINT="$2"
shift 2
;;
*)
echo -e "${RED}Opção desconhecida: $1${NC}"
exit 1
;;
esac
done
# Validações
if [ -z "$BACKUP_FILE" ]; then
echo -e "${RED}Erro: --backup-file é obrigatório${NC}"
exit 1
fi
if [ -z "$BACKUP_URL" ] && [ ! -f "$BACKUP_FILE" ]; then
echo -e "${RED}Erro: --backup-url é obrigatório se o arquivo não existir localmente${NC}"
exit 1
fi
# Funções
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[✓]${NC} $1"
}
log_error() {
echo -e "${RED}[✗]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[!]${NC} $1"
}
cleanup() {
log_info "Limpando arquivos temporários..."
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
# ============ INÍCIO ============
echo -e "${BLUE}"
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Restauração de Backup - Occto Engenharia ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
log_info "Configurações:"
log_info " Banco de Dados: $POSTGRES_DB"
log_info " Host PostgreSQL: $POSTGRES_HOST"
log_info " MinIO: $MINIO_ENDPOINT:$MINIO_PORT"
log_info " Arquivo: $BACKUP_FILE"
echo ""
# 1. Preparar diretório temporário
log_info "Criando diretório temporário..."
mkdir -p "$TEMP_DIR"
log_success "Diretório criado: $TEMP_DIR"
# 2. Baixar backup se necessário
if [ -z "$BACKUP_URL" ] && [ -f "$BACKUP_FILE" ]; then
log_info "Usando arquivo local: $BACKUP_FILE"
cp "$BACKUP_FILE" "$TEMP_DIR/"
else
log_info "Baixando backup de $BACKUP_URL..."
cd "$TEMP_DIR"
curl -# -O "$BACKUP_URL"
log_success "Backup baixado"
fi
# 3. Extrair arquivo
BACKUP_PATH="$TEMP_DIR/$BACKUP_FILE"
if [ ! -f "$BACKUP_PATH" ]; then
log_error "Arquivo não encontrado: $BACKUP_PATH"
exit 1
fi
log_info "Extraindo arquivo ($BACKUP_FILE)..."
cd "$TEMP_DIR"
tar -xzf "$BACKUP_FILE"
log_success "Arquivo extraído"
# 4. Restaurar PostgreSQL
log_info "Conectando ao PostgreSQL ($POSTGRES_HOST:$POSTGRES_PORT)..."
DB_FILE="$TEMP_DIR/database.sql"
if [ ! -f "$DB_FILE" ]; then
log_error "database.sql não encontrado no backup!"
exit 1
fi
log_info "Terminando conexões do banco de dados..."
PGPASSWORD="$POSTGRES_PASSWORD" psql \
-h "$POSTGRES_HOST" \
-p "$POSTGRES_PORT" \
-U "$POSTGRES_USER" \
-tc "SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE datname = '$POSTGRES_DB'
AND pid <> pg_backend_pid();" 2>/dev/null || true
log_info "Removendo banco antigo..."
PGPASSWORD="$POSTGRES_PASSWORD" dropdb \
-h "$POSTGRES_HOST" \
-p "$POSTGRES_PORT" \
-U "$POSTGRES_USER" \
--if-exists "$POSTGRES_DB" 2>/dev/null || true
log_info "Criando novo banco de dados..."
PGPASSWORD="$POSTGRES_PASSWORD" createdb \
-h "$POSTGRES_HOST" \
-p "$POSTGRES_PORT" \
-U "$POSTGRES_USER" \
"$POSTGRES_DB"
log_info "Restaurando dados do PostgreSQL (pode levar alguns minutos)..."
PGPASSWORD="$POSTGRES_PASSWORD" psql \
-h "$POSTGRES_HOST" \
-p "$POSTGRES_PORT" \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
-f "$DB_FILE" > /dev/null 2>&1
log_success "PostgreSQL restaurado com sucesso!"
# 5. Restaurar MinIO (se arquivo existir)
MINIO_DATA_DIR="$TEMP_DIR/minio-data"
if [ -d "$MINIO_DATA_DIR" ]; then
log_info "Restaurando dados do MinIO..."
# Procurar containers Docker do MinIO
MINIO_CONTAINER=$(docker ps --filter "name=$MINIO_ENDPOINT" -q 2>/dev/null || echo "")
if [ -n "$MINIO_CONTAINER" ]; then
log_info "Copiando dados para container MinIO ($MINIO_CONTAINER)..."
docker cp "$MINIO_DATA_DIR/." "$MINIO_CONTAINER:/data/" 2>/dev/null || {
log_warning "Erro ao copiar para MinIO. Você pode fazer manualmente:"
log_warning "docker cp $MINIO_DATA_DIR/. seu-container-minio:/data/"
}
log_info "Reiniciando MinIO..."
docker restart "$MINIO_CONTAINER" > /dev/null 2>&1
log_success "MinIO restaurado!"
else
log_warning "Container MinIO não encontrado. Copie os dados manualmente:"
log_warning "cp -r $MINIO_DATA_DIR/* /seu/minio/data/"
fi
else
log_warning "Pasta minio-data não encontrada no backup"
fi
# 6. Resumo final
echo ""
echo -e "${GREEN}"
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Restauração Concluída com Sucesso! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
log_success "PostgreSQL restaurado"
[ -d "$MINIO_DATA_DIR" ] && log_success "MinIO restaurado"
echo ""
log_info "Próximos passos:"
echo " 1. Reinicie seus containers: docker-compose restart"
echo " 2. Verifique se a aplicação está funcionando"
echo " 3. Acesse http://seu-dominio.com para confirmar"
echo ""