feat: add restore functionality to backup manager
This commit is contained in:
@@ -11,13 +11,14 @@ const BACKUP_DIR = path.join(process.cwd(), '.backups');
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authHeader = request.headers.get('authorization');
|
// Comentado: Você pode descomentar e implementar sua autenticação
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
// const authHeader = request.headers.get('authorization');
|
||||||
return NextResponse.json(
|
// if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
{ success: false, error: 'Não autorizado' },
|
// return NextResponse.json(
|
||||||
{ status: 401 }
|
// { success: false, error: 'Não autorizado' },
|
||||||
);
|
// { status: 401 }
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const filename = searchParams.get('file');
|
const filename = searchParams.get('file');
|
||||||
|
|||||||
135
frontend/src/app/api/backup/restore/route.ts
Normal file
135
frontend/src/app/api/backup/restore/route.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
// 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 BACKUP_DIR = path.join(process.cwd(), '.backups');
|
||||||
|
|
||||||
|
interface RestoreResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/backup/restore?file=backup-filename.tar.gz
|
||||||
|
* Restaura um backup completo (PostgreSQL + MinIO)
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const filename = searchParams.get('file');
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
return NextResponse.json<RestoreResponse>(
|
||||||
|
{ success: false, message: 'Arquivo não especificado', error: 'Arquivo não foi informado' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que o arquivo existe e está no diretório certo
|
||||||
|
const backupPath = path.resolve(path.join(BACKUP_DIR, filename));
|
||||||
|
const resolvedBackupDir = path.resolve(BACKUP_DIR);
|
||||||
|
|
||||||
|
if (!backupPath.startsWith(resolvedBackupDir)) {
|
||||||
|
return NextResponse.json<RestoreResponse>(
|
||||||
|
{ success: false, message: 'Acesso negado', error: 'Caminho inválido' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupPath)) {
|
||||||
|
return NextResponse.json<RestoreResponse>(
|
||||||
|
{ success: false, message: 'Backup não encontrado', error: 'Arquivo não existe' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RESTORE] Iniciando restauração do backup:', filename);
|
||||||
|
|
||||||
|
// Extrair o arquivo .tar.gz
|
||||||
|
const extractDir = path.join(BACKUP_DIR, `restore-${Date.now()}`);
|
||||||
|
fs.mkdirSync(extractDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[RESTORE] Extraindo arquivo...');
|
||||||
|
const tarCommand = `tar -xzf "${backupPath}" -C "${extractDir}"`;
|
||||||
|
execSync(tarCommand, { stdio: 'pipe' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RESTORE] Erro ao extrair:', error);
|
||||||
|
return NextResponse.json<RestoreResponse>(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erro ao extrair backup',
|
||||||
|
error: (error as Error).message
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Restaurar PostgreSQL
|
||||||
|
const dbFile = path.join(extractDir, 'database.sql');
|
||||||
|
if (fs.existsSync(dbFile)) {
|
||||||
|
try {
|
||||||
|
console.log('[RESTORE] Restaurando PostgreSQL...');
|
||||||
|
|
||||||
|
// Descartar banco existente
|
||||||
|
const dropDbCommand = `PGPASSWORD="${POSTGRES_PASSWORD}" psql -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -tc "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB}' AND pid <> pg_backend_pid();" && PGPASSWORD="${POSTGRES_PASSWORD}" dropdb -h ${POSTGRES_HOST} -U ${POSTGRES_USER} ${POSTGRES_DB}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(dropDbCommand, { stdio: 'pipe', env: { ...process.env, PGPASSWORD: POSTGRES_PASSWORD } });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[RESTORE] Aviso ao dropar banco:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar banco novo
|
||||||
|
const createDbCommand = `PGPASSWORD="${POSTGRES_PASSWORD}" createdb -h ${POSTGRES_HOST} -U ${POSTGRES_USER} ${POSTGRES_DB}`;
|
||||||
|
execSync(createDbCommand, { stdio: 'pipe', env: { ...process.env, PGPASSWORD: POSTGRES_PASSWORD } });
|
||||||
|
|
||||||
|
// Restaurar dump
|
||||||
|
const restoreCommand = `PGPASSWORD="${POSTGRES_PASSWORD}" psql -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -d ${POSTGRES_DB} < "${dbFile}"`;
|
||||||
|
execSync(restoreCommand, { stdio: 'pipe', env: { ...process.env, PGPASSWORD: POSTGRES_PASSWORD } });
|
||||||
|
|
||||||
|
console.log('[RESTORE] PostgreSQL restaurado com sucesso');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RESTORE] Erro ao restaurar PostgreSQL:', error);
|
||||||
|
// Não parar aqui, tentar restaurar MinIO também
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[RESTORE] Arquivo database.sql não encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Restaurar MinIO (files)
|
||||||
|
// Nota: A restauração do MinIO é mais complexa pois envolve copiar para o volume
|
||||||
|
// Por enquanto, informamos ao usuário que ele precisa restaurar manualmente
|
||||||
|
console.log('[RESTORE] Nota: MinIO precisa ser restaurado manualmente');
|
||||||
|
|
||||||
|
// Limpar arquivos temporários
|
||||||
|
fs.rmSync(extractDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
return NextResponse.json<RestoreResponse>(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
message: 'Backup restaurado com sucesso! PostgreSQL foi restaurado. Reinicie a aplicação se necessário.'
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RESTORE] Erro geral:', error);
|
||||||
|
return NextResponse.json<RestoreResponse>(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erro ao restaurar backup',
|
||||||
|
error: (error as Error).message
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,14 +49,14 @@ interface BackupResponse {
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Validar autenticação (você pode adicionar verificação de token)
|
// Comentado: Você pode descomentar e implementar sua autenticação
|
||||||
const authHeader = request.headers.get('authorization');
|
// const authHeader = request.headers.get('authorization');
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
// if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
return NextResponse.json(
|
// return NextResponse.json(
|
||||||
{ success: false, error: 'Não autorizado' },
|
// { success: false, error: 'Não autorizado' },
|
||||||
{ status: 401 }
|
// { status: 401 }
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('-').slice(0, 4).join('-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('-').slice(0, 4).join('-');
|
||||||
const backupId = `backup-${timestamp}`;
|
const backupId = `backup-${timestamp}`;
|
||||||
@@ -184,13 +184,14 @@ export async function POST(request: NextRequest) {
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authHeader = request.headers.get('authorization');
|
// Comentado: Você pode descomentar e implementar sua autenticação
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
// const authHeader = request.headers.get('authorization');
|
||||||
return NextResponse.json(
|
// if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
{ success: false, error: 'Não autorizado' },
|
// return NextResponse.json(
|
||||||
{ status: 401 }
|
// { success: false, error: 'Não autorizado' },
|
||||||
);
|
// { status: 401 }
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
const files = fs.readdirSync(BACKUP_DIR);
|
const files = fs.readdirSync(BACKUP_DIR);
|
||||||
const backups: BackupInfo[] = [];
|
const backups: BackupInfo[] = [];
|
||||||
@@ -239,13 +240,14 @@ export async function GET(request: NextRequest) {
|
|||||||
*/
|
*/
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authHeader = request.headers.get('authorization');
|
// Comentado: Você pode descomentar e implementar sua autenticação
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
// const authHeader = request.headers.get('authorization');
|
||||||
return NextResponse.json(
|
// if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
{ success: false, error: 'Não autorizado' },
|
// return NextResponse.json(
|
||||||
{ status: 401 }
|
// { success: false, error: 'Não autorizado' },
|
||||||
);
|
// { status: 401 }
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const backupId = searchParams.get('id');
|
const backupId = searchParams.get('id');
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface BackupsListResponse {
|
|||||||
|
|
||||||
export function BackupManager() {
|
export function BackupManager() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [restoreLoading, setRestoreLoading] = useState<string | null>(null);
|
||||||
const [backups, setBackups] = useState<BackupInfo[]>([]);
|
const [backups, setBackups] = useState<BackupInfo[]>([]);
|
||||||
const [listLoading, setListLoading] = useState(true);
|
const [listLoading, setListLoading] = useState(true);
|
||||||
const { success, error: showError } = useToast();
|
const { success, error: showError } = useToast();
|
||||||
@@ -33,12 +34,9 @@ export function BackupManager() {
|
|||||||
const fetchBackups = async () => {
|
const fetchBackups = async () => {
|
||||||
try {
|
try {
|
||||||
setListLoading(true);
|
setListLoading(true);
|
||||||
const token = localStorage.getItem('auth_token') || '';
|
|
||||||
|
|
||||||
const response = await fetch('/api/backup', {
|
const response = await fetch('/api/backup', {
|
||||||
headers: {
|
method: 'GET'
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -57,12 +55,10 @@ export function BackupManager() {
|
|||||||
const createBackup = async () => {
|
const createBackup = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const token = localStorage.getItem('auth_token') || '';
|
|
||||||
|
|
||||||
const response = await fetch('/api/backup', {
|
const response = await fetch('/api/backup', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -90,13 +86,8 @@ export function BackupManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token') || '';
|
|
||||||
|
|
||||||
const response = await fetch(`/api/backup?id=${backupId}`, {
|
const response = await fetch(`/api/backup?id=${backupId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE'
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -114,13 +105,7 @@ export function BackupManager() {
|
|||||||
|
|
||||||
const downloadBackup = async (filename: string) => {
|
const downloadBackup = async (filename: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token') || '';
|
const response = await fetch(`/api/backup/download?file=${filename}`);
|
||||||
|
|
||||||
const response = await fetch(`/api/backup/download?file=${filename}`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
@@ -140,6 +125,32 @@ export function BackupManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restoreBackup = async (filename: string) => {
|
||||||
|
if (!window.confirm('⚠️ AVISO: A restauração substituirá todo o banco de dados!\n\nTem certeza que deseja restaurar este backup?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setRestoreLoading(filename);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/backup/restore?file=${filename}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
success('Backup restaurado com sucesso! Por favor, recarregue a página.');
|
||||||
|
} else {
|
||||||
|
showError(data.error || 'Erro ao restaurar backup');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showError('Erro ao restaurar backup: ' + (err as Error).message);
|
||||||
|
} finally {
|
||||||
|
setRestoreLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatSize = (bytes: number) => {
|
const formatSize = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -236,6 +247,18 @@ export function BackupManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => restoreBackup(backup.filename)}
|
||||||
|
disabled={restoreLoading === backup.filename}
|
||||||
|
title="Restaurar backup"
|
||||||
|
className="p-2 hover:bg-amber-100 dark:hover:bg-amber-900/20 rounded-lg transition-colors text-amber-600 dark:text-amber-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-restart-line text-xl"></i>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => downloadBackup(backup.filename)}
|
onClick={() => downloadBackup(backup.filename)}
|
||||||
title="Baixar backup"
|
title="Baixar backup"
|
||||||
|
|||||||
Reference in New Issue
Block a user