feat: adicionar sistema de backup e badge editável na página inicial

This commit is contained in:
Erik
2025-11-29 12:22:56 -03:00
parent b73eb6c3eb
commit 99530200b4
13 changed files with 1511 additions and 41 deletions

View File

@@ -0,0 +1,334 @@
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 {
// Validar autenticação (você pode adicionar verificação de token)
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<BackupResponse>(
{
success: false,
message: 'Backup concluído com erros',
backup: backupInfo,
error: backupInfo.message
},
{ status: 207 } // Multi-status
);
}
return NextResponse.json<BackupResponse>(
{
success: true,
message: 'Backup realizado com sucesso',
backup: backupInfo
},
{ status: 201 }
);
} catch (error) {
console.error('[BACKUP] Erro geral:', error);
return NextResponse.json<BackupResponse>(
{
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 {
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 {
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;
}