337 lines
9.9 KiB
TypeScript
337 lines
9.9 KiB
TypeScript
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<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 {
|
|
// 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;
|
|
}
|