Agregar funcionalidad de importación de backups
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
Implementa la capacidad de restaurar la base de datos desde un archivo SQL de backup exportado previamente. Cambios: Backend: - Nuevo endpoint POST /api/debug/import-database.post.ts - Recibe archivo .sql via multipart/form-data - Ejecuta el SQL del backup (que incluye DROP y CREATE) - Reemplaza completamente la base de datos existente Frontend: - Nuevo botón verde "📥 IMPORTAR BACKUP" - Input file oculto con accept=".sql" - Validación de extensión de archivo - Confirmación con advertencia clara - Estado de carga durante importación Flujo de uso: 1. Usuario hace click en "📥 IMPORTAR BACKUP" 2. Selecciona archivo .sql previamente exportado 3. Confirma que desea reemplazar toda la BD 4. Sistema ejecuta el SQL completo 5. Muestra mensaje de éxito/error 6. Usuario recarga la página Esto completa el ciclo backup/restore permitiendo recuperación completa del estado de la base de datos.
This commit is contained in:
@@ -87,6 +87,21 @@
|
|||||||
>
|
>
|
||||||
💾 EXPORTAR BACKUP
|
💾 EXPORTAR BACKUP
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
@click="triggerFileInput"
|
||||||
|
color="green"
|
||||||
|
variant="solid"
|
||||||
|
:loading="importingDB"
|
||||||
|
>
|
||||||
|
📥 IMPORTAR BACKUP
|
||||||
|
</UButton>
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
type="file"
|
||||||
|
accept=".sql"
|
||||||
|
@change="importDatabase"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-red-600 dark:text-red-400 mt-2">
|
<p class="text-xs text-red-600 dark:text-red-400 mt-2">
|
||||||
Resultados en consola (F12). Recarga la página después de usar estos botones.
|
Resultados en consola (F12). Recarga la página después de usar estos botones.
|
||||||
@@ -459,6 +474,8 @@ const resettingDB = ref(false)
|
|||||||
const seedingDB = ref(false)
|
const seedingDB = ref(false)
|
||||||
const clearingData = ref(false)
|
const clearingData = ref(false)
|
||||||
const exportingDB = ref(false)
|
const exportingDB = ref(false)
|
||||||
|
const importingDB = ref(false)
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const resetDatabase = async () => {
|
const resetDatabase = async () => {
|
||||||
if (!confirm('⚠️ ADVERTENCIA: Esto BORRARÁ TODOS LOS DATOS de la base de datos.\n\n¿Estás seguro de continuar?')) {
|
if (!confirm('⚠️ ADVERTENCIA: Esto BORRARÁ TODOS LOS DATOS de la base de datos.\n\n¿Estás seguro de continuar?')) {
|
||||||
@@ -574,6 +591,61 @@ const exportDatabase = async () => {
|
|||||||
exportingDB.value = false
|
exportingDB.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const triggerFileInput = () => {
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDatabase = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.name.endsWith('.sql')) {
|
||||||
|
alert('⚠️ Por favor selecciona un archivo .sql')
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('⚠️ ADVERTENCIA: Esto REEMPLAZARÁ TODA LA BASE DE DATOS con el contenido del backup.\n\nSe eliminará todo lo existente y se cargará el backup.\n\n¿Estás seguro de continuar?')) {
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📥 === IMPORTANDO BACKUP DE BASE DE DATOS ===')
|
||||||
|
console.log(` - Archivo: ${file.name}`)
|
||||||
|
console.log(` - Tamaño: ${(file.size / 1024).toFixed(2)} KB`)
|
||||||
|
importingDB.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await fetch('/api/debug/import-database', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('Status:', response.status)
|
||||||
|
console.log('Respuesta:', data)
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ Backup importado exitosamente.\n\nLa base de datos ha sido restaurada.\n\nRecarga la página para ver los cambios.')
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Error desconocido')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Error:', error)
|
||||||
|
alert(`❌ Error importando backup: ${error.message || 'Ver consola para más detalles'}`)
|
||||||
|
} finally {
|
||||||
|
importingDB.value = false
|
||||||
|
input.value = '' // Limpiar el input
|
||||||
|
}
|
||||||
|
}
|
||||||
// ⚠️⚠️⚠️ FIN FUNCIONES DE DEBUG ⚠️⚠️⚠️
|
// ⚠️⚠️⚠️ FIN FUNCIONES DE DEBUG ⚠️⚠️⚠️
|
||||||
|
|
||||||
// Funciones de prueba de API
|
// Funciones de prueba de API
|
||||||
|
|||||||
97
nuxt4/server/api/debug/import-database.post.ts
Normal file
97
nuxt4/server/api/debug/import-database.post.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* ⚠️ ⚠️ ⚠️ ENDPOINT DE DEBUG - TEMPORAL ⚠️ ⚠️ ⚠️
|
||||||
|
*
|
||||||
|
* POST /api/debug/import-database
|
||||||
|
*
|
||||||
|
* IMPORTA UN BACKUP SQL COMPLETO (reemplaza todo lo existente)
|
||||||
|
*
|
||||||
|
* ⚠️ NO ELIMINAR SIN CONSULTAR A DARIO/DRAGANEL/NUCLEO000 ⚠️
|
||||||
|
*
|
||||||
|
* Este endpoint fue creado para desarrollo y debugging.
|
||||||
|
* Antes de eliminarlo, preguntar si todavía es necesario.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getClient } from '../../utils/db'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
console.log('📥 IMPORT DATABASE - Importando backup SQL...')
|
||||||
|
|
||||||
|
// Leer el archivo del request
|
||||||
|
const formData = await readMultipartFormData(event)
|
||||||
|
|
||||||
|
if (!formData || formData.length === 0) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'No se recibió ningún archivo',
|
||||||
|
data: { message: 'Debes enviar un archivo .sql en el campo "file"' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileData = formData.find(item => item.name === 'file')
|
||||||
|
|
||||||
|
if (!fileData) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Campo "file" no encontrado',
|
||||||
|
data: { message: 'El archivo debe enviarse en el campo "file"' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqlContent = fileData.data.toString('utf-8')
|
||||||
|
const fileSize = (sqlContent.length / 1024).toFixed(2)
|
||||||
|
|
||||||
|
console.log(` - Archivo recibido: ${fileData.filename || 'sin nombre'}`)
|
||||||
|
console.log(` - Tamaño: ${fileSize} KB`)
|
||||||
|
|
||||||
|
if (!sqlContent.trim()) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Archivo vacío',
|
||||||
|
data: { message: 'El archivo SQL está vacío' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await getClient()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN')
|
||||||
|
|
||||||
|
console.log(' - Ejecutando SQL del backup...')
|
||||||
|
console.log(' - ADVERTENCIA: Esto eliminará y recreará todas las tablas')
|
||||||
|
|
||||||
|
// Ejecutar el SQL del backup
|
||||||
|
// Los backups de pg_dump con --clean --if-exists ya incluyen los DROP necesarios
|
||||||
|
await client.query(sqlContent)
|
||||||
|
|
||||||
|
await client.query('COMMIT')
|
||||||
|
|
||||||
|
console.log('✅ Backup importado exitosamente')
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Backup importado exitosamente. La base de datos ha sido restaurada.',
|
||||||
|
file_size: fileSize,
|
||||||
|
filename: fileData.filename || 'sin nombre',
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK')
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Error importando backup:', error)
|
||||||
|
|
||||||
|
// Si el error ya es un createError, reusarlo
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Error importando backup',
|
||||||
|
data: { message: error.message },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user