All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m5s
Resuelve problemas de compatibilidad entre versiones de PostgreSQL al exportar e importar backups. Cambios en export-database.post.ts: - Agregar flags adicionales a pg_dump para mayor portabilidad: --no-tablespaces: evita referencias a tablespaces --no-security-labels: evita security labels --no-synchronized-snapshots: evita snapshots sincronizados - Esto genera SQL más compatible entre diferentes instalaciones Cambios en import-database.post.ts: - Limpiar SQL antes de importar, removiendo comandos incompatibles: * SET transaction_timeout (no existe en todas las versiones) * SET idle_in_transaction_session_timeout * SET lock_timeout * \unrestrict (comando no reconocido) - Agregar -q y -v ON_ERROR_STOP=1 a psql - Mejorar detección de errores: solo fallar en ERROR/FATAL reales - Reportar tamaño original y limpio del archivo Con estos cambios, los backups exportados desde cualquier versión de PostgreSQL se pueden importar correctamente sin errores de compatibilidad.
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
/**
|
|
* ⚠️ ⚠️ ⚠️ 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 { exec } from 'child_process'
|
|
import { promisify } from 'util'
|
|
import { writeFile, unlink } from 'fs/promises'
|
|
import { join } from 'path'
|
|
|
|
const execAsync = promisify(exec)
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
let tempFilePath: string | null = null
|
|
|
|
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 originalSqlContent = fileData.data.toString('utf-8')
|
|
const originalFileSize = (originalSqlContent.length / 1024).toFixed(2)
|
|
|
|
console.log(` - Archivo recibido: ${fileData.filename || 'sin nombre'}`)
|
|
console.log(` - Tamaño original: ${originalFileSize} KB`)
|
|
|
|
if (!originalSqlContent.trim()) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Archivo vacío',
|
|
data: { message: 'El archivo SQL está vacío' },
|
|
})
|
|
}
|
|
|
|
// Limpiar el SQL de comandos problemáticos o incompatibles
|
|
console.log(' - Limpiando SQL de comandos incompatibles...')
|
|
const sqlLines = originalSqlContent.split('\n')
|
|
const cleanedLines = sqlLines.filter(line => {
|
|
const trimmed = line.trim()
|
|
// Remover líneas problemáticas conocidas
|
|
if (trimmed.startsWith('SET transaction_timeout')) return false
|
|
if (trimmed.startsWith('SET idle_in_transaction_session_timeout')) return false
|
|
if (trimmed.startsWith('SET lock_timeout')) return false
|
|
if (trimmed.startsWith('\\unrestrict')) return false
|
|
return true
|
|
})
|
|
|
|
const sqlContent = cleanedLines.join('\n')
|
|
const cleanedFileSize = (sqlContent.length / 1024).toFixed(2)
|
|
console.log(` - Tamaño después de limpieza: ${cleanedFileSize} KB`)
|
|
|
|
// Guardar el archivo temporalmente
|
|
const timestamp = Date.now()
|
|
tempFilePath = join('/tmp', `import-${timestamp}.sql`)
|
|
await writeFile(tempFilePath, sqlContent, 'utf-8')
|
|
|
|
console.log(` - Archivo guardado temporalmente en: ${tempFilePath}`)
|
|
|
|
// Obtener credenciales de la base de datos
|
|
const config = useRuntimeConfig()
|
|
let host: string
|
|
let port: string
|
|
let database: string
|
|
let user: string
|
|
let password: string
|
|
|
|
if (config.postgresUrl) {
|
|
// Parsear la URL de conexión
|
|
const url = new URL(config.postgresUrl)
|
|
host = url.hostname
|
|
port = url.port || '5432'
|
|
database = url.pathname.slice(1)
|
|
user = url.username
|
|
password = url.password
|
|
} else {
|
|
// Fallback a variables de entorno
|
|
const defaultHost = process.env.APP_NAME ? `${process.env.APP_NAME}-postgres` : 'postgres'
|
|
host = process.env.POSTGRES_HOST || defaultHost
|
|
port = process.env.POSTGRES_PORT || '5432'
|
|
database = process.env.POSTGRES_DB || 'seguidor_lotes'
|
|
user = process.env.POSTGRES_USER || 'seguidor'
|
|
password = process.env.POSTGRES_PASSWORD || 'seguidor_password'
|
|
}
|
|
|
|
console.log(` - Host: ${host}`)
|
|
console.log(` - Puerto: ${port}`)
|
|
console.log(` - Base de datos: ${database}`)
|
|
console.log(` - Usuario: ${user}`)
|
|
|
|
// Ejecutar psql para importar el backup
|
|
console.log(' - Ejecutando psql para importar el backup...')
|
|
console.log(' - ADVERTENCIA: Esto eliminará y recreará todas las tablas')
|
|
|
|
// Opciones de psql:
|
|
// -q: quiet mode (menos mensajes)
|
|
// -v ON_ERROR_STOP=1: detener en el primer error crítico
|
|
const command = `PGPASSWORD="${password}" psql -h ${host} -p ${port} -U ${user} -d ${database} -q -v ON_ERROR_STOP=1 -f ${tempFilePath}`
|
|
|
|
const { stdout, stderr } = await execAsync(command, {
|
|
maxBuffer: 50 * 1024 * 1024, // 50MB buffer
|
|
})
|
|
|
|
// psql envía algunos mensajes normales a stderr
|
|
// Solo fallar si hay errores críticos (ERROR en mayúsculas o 'error:' en minúsculas)
|
|
if (stderr) {
|
|
const hasError = stderr.includes('ERROR:') || stderr.includes('error:') || stderr.includes('FATAL:')
|
|
|
|
if (hasError) {
|
|
console.error('⚠️ Errores durante la importación:', stderr)
|
|
throw new Error(`Error ejecutando psql: ${stderr}`)
|
|
} else {
|
|
// Son solo warnings o notices, no son críticos
|
|
console.log(' - Mensajes de psql (no críticos):', stderr)
|
|
}
|
|
}
|
|
|
|
console.log('✅ Backup importado exitosamente')
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Backup importado exitosamente. La base de datos ha sido restaurada.',
|
|
original_size: originalFileSize,
|
|
cleaned_size: cleanedFileSize,
|
|
filename: fileData.filename || 'sin nombre',
|
|
}
|
|
} catch (error: any) {
|
|
console.error('❌ Error importando backup:', error)
|
|
|
|
// Si el error es porque psql no está disponible
|
|
if (error.message?.includes('psql') && error.message?.includes('not found')) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'psql no está disponible',
|
|
data: {
|
|
message: 'El comando psql no está disponible en el servidor. Asegúrate de que PostgreSQL client tools estén instalados.',
|
|
},
|
|
})
|
|
}
|
|
|
|
// 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 },
|
|
})
|
|
} finally {
|
|
// Limpiar el archivo temporal
|
|
if (tempFilePath) {
|
|
try {
|
|
await unlink(tempFilePath)
|
|
console.log(` - Archivo temporal eliminado: ${tempFilePath}`)
|
|
} catch (unlinkError) {
|
|
console.warn('⚠️ No se pudo eliminar el archivo temporal:', unlinkError)
|
|
}
|
|
}
|
|
}
|
|
})
|