feat: add cloud backup upload and universal restore script
This commit is contained in:
131
frontend/src/app/api/backup/upload/route.ts
Normal file
131
frontend/src/app/api/backup/upload/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user