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

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}