import { NextRequest, NextResponse } from 'next/server'; import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { createReadStream } from 'fs'; // Variáveis de ambiente const POSTGRES_USER = process.env.POSTGRES_USER || 'admin'; const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || 'adminpassword'; const POSTGRES_DB = process.env.POSTGRES_DB || 'occto_db'; const POSTGRES_HOST = process.env.POSTGRES_HOST || 'postgres'; const POSTGRES_PORT = process.env.POSTGRES_PORT || '5432'; 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_BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'occto-images'; const MINIO_USE_SSL = process.env.MINIO_USE_SSL === 'true'; // Diretório para armazenar backups const BACKUP_DIR = path.join(process.cwd(), '.backups'); // Criar diretório de backups se não existir if (!fs.existsSync(BACKUP_DIR)) { fs.mkdirSync(BACKUP_DIR, { recursive: true }); } interface BackupInfo { id: string; timestamp: string; date: string; size: number; filename: string; status: 'success' | 'error'; message: string; } interface BackupResponse { success: boolean; message: string; backup?: BackupInfo; error?: string; } /** * POST /api/backup * Cria um backup completo do PostgreSQL e MinIO */ export async function POST(request: NextRequest) { try { // Comentado: Você pode descomentar e implementar sua autenticação // const authHeader = request.headers.get('authorization'); // if (!authHeader || !authHeader.startsWith('Bearer ')) { // return NextResponse.json( // { success: false, error: 'Não autorizado' }, // { status: 401 } // ); // } const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('-').slice(0, 4).join('-'); const backupId = `backup-${timestamp}`; const backupPath = path.join(BACKUP_DIR, backupId); // Criar pasta do backup fs.mkdirSync(backupPath, { recursive: true }); const backupInfo: BackupInfo = { id: backupId, timestamp: new Date().toISOString(), date: new Date().toLocaleDateString('pt-BR'), size: 0, filename: backupId, status: 'success', message: '' }; // 1. Fazer backup do PostgreSQL try { console.log('[BACKUP] Iniciando backup do PostgreSQL...'); const pgBackupPath = path.join(backupPath, 'database.sql'); const pgCommand = `PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -d ${POSTGRES_DB} > "${pgBackupPath}"`; execSync(pgCommand, { stdio: 'pipe', env: { ...process.env, PGPASSWORD: POSTGRES_PASSWORD } }); console.log('[BACKUP] PostgreSQL backup concluído'); } catch (error) { console.error('[BACKUP] Erro ao fazer backup do PostgreSQL:', error); backupInfo.status = 'error'; backupInfo.message += `Erro PostgreSQL: ${(error as Error).message}. `; } // 2. Fazer backup do MinIO (copiar os dados) try { console.log('[BACKUP] Iniciando backup do MinIO...'); const minioBackupPath = path.join(backupPath, 'minio-data'); // Se estiver usando Docker, copiar do volume // Se estiver local, copiar do diretório minio_data const minioDataPath = path.join(process.cwd(), '..', 'minio_data'); if (fs.existsSync(minioDataPath)) { // Copiar recursivamente copyDirSync(minioDataPath, minioBackupPath); console.log('[BACKUP] MinIO backup concluído'); } else { console.warn('[BACKUP] Diretório MinIO não encontrado em:', minioDataPath); backupInfo.message += `Aviso: MinIO local não encontrado. `; } } catch (error) { console.error('[BACKUP] Erro ao fazer backup do MinIO:', error); backupInfo.message += `Erro MinIO: ${(error as Error).message}. `; } // 3. Criar arquivo metadata.json const metadataPath = path.join(backupPath, 'metadata.json'); fs.writeFileSync(metadataPath, JSON.stringify({ timestamp: backupInfo.timestamp, database: POSTGRES_DB, hostname: POSTGRES_HOST, minioEndpoint: MINIO_ENDPOINT, version: '1.0' }, null, 2)); // 4. Calcular tamanho do backup const size = calculateDirSize(backupPath); backupInfo.size = size; // 5. Compactar backup (opcional - melhor para armazenamento) const compressBackup = true; if (compressBackup) { try { console.log('[BACKUP] Compactando backup...'); const tarCommand = `tar -czf "${path.join(BACKUP_DIR, backupId)}.tar.gz" -C "${BACKUP_DIR}" "${backupId}"`; execSync(tarCommand, { stdio: 'pipe' }); // Remover pasta original após compactar fs.rmSync(backupPath, { recursive: true, force: true }); backupInfo.filename = `${backupId}.tar.gz`; console.log('[BACKUP] Compactação concluída'); } catch (error) { console.warn('[BACKUP] Erro ao compactar:', error); backupInfo.message += `Aviso: Não foi possível compactar. `; } } if (backupInfo.status === 'error' && backupInfo.message) { return NextResponse.json( { success: false, message: 'Backup concluído com erros', backup: backupInfo, error: backupInfo.message }, { status: 207 } // Multi-status ); } return NextResponse.json( { success: true, message: 'Backup realizado com sucesso', backup: backupInfo }, { status: 201 } ); } catch (error) { console.error('[BACKUP] Erro geral:', error); return NextResponse.json( { success: false, message: 'Erro ao criar backup', error: (error as Error).message }, { status: 500 } ); } } /** * GET /api/backup * Lista os backups disponíveis */ export async function GET(request: NextRequest) { try { // Comentado: Você pode descomentar e implementar sua autenticação // const authHeader = request.headers.get('authorization'); // if (!authHeader || !authHeader.startsWith('Bearer ')) { // return NextResponse.json( // { success: false, error: 'Não autorizado' }, // { status: 401 } // ); // } const files = fs.readdirSync(BACKUP_DIR); const backups: BackupInfo[] = []; for (const file of files) { const filePath = path.join(BACKUP_DIR, file); const stat = fs.statSync(filePath); if (stat.isFile()) { const timestamp = file.replace('backup-', '').replace('.tar.gz', ''); backups.push({ id: file.replace('.tar.gz', ''), timestamp: new Date(timestamp.replace(/-/g, ':')).toISOString(), date: new Date(timestamp.replace(/-/g, ':')).toLocaleDateString('pt-BR'), size: stat.size, filename: file, status: 'success', message: 'Backup disponível' }); } } // Ordenar por data (mais recente primeiro) backups.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); return NextResponse.json({ success: true, backups, count: backups.length }); } catch (error) { console.error('[BACKUP] Erro ao listar backups:', error); return NextResponse.json( { success: false, error: (error as Error).message }, { status: 500 } ); } } /** * DELETE /api/backup?id=backup-id * Remove um backup específico */ export async function DELETE(request: NextRequest) { try { // Comentado: Você pode descomentar e implementar sua autenticação // const authHeader = request.headers.get('authorization'); // if (!authHeader || !authHeader.startsWith('Bearer ')) { // return NextResponse.json( // { success: false, error: 'Não autorizado' }, // { status: 401 } // ); // } const { searchParams } = new URL(request.url); const backupId = searchParams.get('id'); if (!backupId) { return NextResponse.json( { success: false, error: 'ID do backup não fornecido' }, { status: 400 } ); } const backupPath = path.join(BACKUP_DIR, `${backupId}.tar.gz`); if (!fs.existsSync(backupPath)) { return NextResponse.json( { success: false, error: 'Backup não encontrado' }, { status: 404 } ); } fs.unlinkSync(backupPath); return NextResponse.json({ success: true, message: 'Backup removido com sucesso' }); } catch (error) { console.error('[BACKUP] Erro ao deletar backup:', error); return NextResponse.json( { success: false, error: (error as Error).message }, { status: 500 } ); } } /** * Função auxiliar: copiar diretório recursivamente */ function copyDirSync(src: string, dest: string) { if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } const files = fs.readdirSync(src); for (const file of files) { const srcPath = path.join(src, file); const destPath = path.join(dest, file); const stat = fs.statSync(srcPath); if (stat.isDirectory()) { copyDirSync(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); } } } /** * Função auxiliar: calcular tamanho do diretório */ function calculateDirSize(dirPath: string): number { let size = 0; try { const files = fs.readdirSync(dirPath); for (const file of files) { const filePath = path.join(dirPath, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { size += calculateDirSize(filePath); } else { size += stat.size; } } } catch (error) { console.error('Erro ao calcular tamanho:', error); } return size; }