feature/generador-instaladores-linux-windows
This commit is contained in:
319
install.ps1
319
install.ps1
@@ -1,124 +1,227 @@
|
||||
# Script de instalación rápida
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Instalador Windows de CloudRestoreAS.
|
||||
|
||||
Write-Host "===============================================" -ForegroundColor Cyan
|
||||
Write-Host "CloudRestoreAS - Instalación Rápida" -ForegroundColor Cyan
|
||||
Write-Host "===============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
.DESCRIPTION
|
||||
NO instala ni descarga NADA en el sistema: el binario es 100% autocontenido (Qt,
|
||||
driver ODBC + Kerberos/OpenSSL, y 7-Zip van embebidos). Este script solo coloca el
|
||||
.exe, hace el bootstrap de config\, opcionalmente siembra las credenciales del PANEL
|
||||
y registra el arranque automático.
|
||||
|
||||
# Verificar Python
|
||||
Write-Host "1. Verificando Python..." -ForegroundColor Yellow
|
||||
$pythonVersion = python --version 2>$null
|
||||
if (-not $pythonVersion) {
|
||||
Write-Host "❌ Python no está instalado o no está en el PATH" -ForegroundColor Red
|
||||
Write-Host " Descarga Python 3.11+ desde: https://www.python.org/downloads/" -ForegroundColor Yellow
|
||||
Read-Host "Presiona Enter para salir..."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ $pythonVersion" -ForegroundColor Green
|
||||
Por eso tampoco se usa NSSM ni ningún envoltorio de servicio descargado: el arranque
|
||||
24/7 se resuelve con una tarea programada ONSTART, que ya viene en el SO.
|
||||
|
||||
# Verificar versión de Python
|
||||
$versionString = $pythonVersion -replace "Python ", ""
|
||||
$version = [version]($versionString.Split()[0])
|
||||
if ($version -lt [version]"3.11") {
|
||||
Write-Host "❌ Python $version es demasiado antiguo. Se requiere 3.11+" -ForegroundColor Red
|
||||
Read-Host "Presiona Enter para salir..."
|
||||
exit 1
|
||||
}
|
||||
.PARAMETER Service
|
||||
Arranque 24/7 sin sesión: tarea programada ONSTART como SYSTEM (recomendado en servidor).
|
||||
|
||||
# Verificar 7-Zip
|
||||
Write-Host ""
|
||||
Write-Host "2. Verificando 7-Zip..." -ForegroundColor Yellow
|
||||
$sevenZipPaths = @(
|
||||
"C:\Program Files\7-Zip\7z.exe",
|
||||
"D:\Program Files\7-Zip\7z.exe",
|
||||
"C:\Program Files (x86)\7-Zip\7z.exe"
|
||||
.PARAMETER Desktop
|
||||
Arranque al iniciar sesión. La app registra su propia tarea ONLOGON al ejecutarse.
|
||||
|
||||
.PARAMETER Prefix
|
||||
Carpeta destino. Default C:\Aduanasoft\CloudRestoreAS.
|
||||
|
||||
.PARAMETER PanelEnvFile
|
||||
Archivo KEY=valor con CLOUDRESTORE_PANEL_API_URL / _API_TOKEN / _INSTANCE_KEY que se
|
||||
fusiona en config\.env tras el bootstrap y luego se borra. Lo usa el instalador remoto
|
||||
del PANEL para dejar el servidor configurado sin intervención.
|
||||
|
||||
.EXAMPLE
|
||||
.\install.ps1 -Service
|
||||
.EXAMPLE
|
||||
.\install.ps1 -Desktop -Prefix 'D:\CloudRestoreAS'
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Service,
|
||||
[switch]$Desktop,
|
||||
[string]$Prefix = 'C:\Aduanasoft\CloudRestoreAS',
|
||||
[string]$PanelEnvFile = ''
|
||||
)
|
||||
$sevenZipFound = $false
|
||||
foreach ($path in $sevenZipPaths) {
|
||||
if (Test-Path $path) {
|
||||
Write-Host "✅ 7-Zip encontrado en: $path" -ForegroundColor Green
|
||||
$sevenZipFound = $true
|
||||
break
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$BinName = 'CloudRestoreAS.exe'
|
||||
$TaskName = 'CloudRestoreAS'
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
|
||||
function Write-Ok($msg) { Write-Host "OK: $msg" -ForegroundColor Green }
|
||||
function Write-Warn($msg) { Write-Warning $msg }
|
||||
|
||||
if ($Service -and $Desktop) {
|
||||
throw 'Elige -Service o -Desktop, no ambos.'
|
||||
}
|
||||
$Mode = if ($Service) { 'service' } elseif ($Desktop) { 'desktop' } else { 'none' }
|
||||
|
||||
# --- Localizar el binario (mismas rutas candidatas que install.sh) -------------------
|
||||
$candidates = @(
|
||||
(Join-Path $ScriptDir "dist\$BinName"),
|
||||
(Join-Path $ScriptDir $BinName)
|
||||
)
|
||||
$src = $candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
|
||||
if (-not $src) {
|
||||
throw "No se encontró el binario ($BinName). Ejecuta .\build.ps1 primero."
|
||||
}
|
||||
|
||||
# -Service crea una tarea como SYSTEM: requiere elevación.
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal] `
|
||||
[Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if ($Mode -eq 'service' -and -not $isAdmin) {
|
||||
throw 'Se requiere PowerShell como Administrador para -Service (tarea ONSTART como SYSTEM).'
|
||||
}
|
||||
|
||||
Write-Host '===============================================' -ForegroundColor Cyan
|
||||
Write-Host 'CloudRestoreAS - Instalación Windows'
|
||||
Write-Host " Binario : $src"
|
||||
Write-Host " Destino : $Prefix"
|
||||
Write-Host " Modo : $Mode"
|
||||
Write-Host '===============================================' -ForegroundColor Cyan
|
||||
|
||||
# --- Colocar el binario -------------------------------------------------------------
|
||||
Write-Step 'Instalando binario'
|
||||
New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
|
||||
$dest = Join-Path $Prefix $BinName
|
||||
|
||||
# Si hay una instancia corriendo, el .exe queda bloqueado y Copy-Item falla. Se detiene
|
||||
# la tarea y se esperan los procesos antes de reemplazar (caso actualización).
|
||||
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
|
||||
Write-Step 'Deteniendo tarea programada existente'
|
||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
}
|
||||
Get-Process -Name 'CloudRestoreAS' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$_ | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
# Espera acotada a que el SO libere el archivo; sin esto el Copy-Item puede fallar.
|
||||
for ($i = 0; $i -lt 10; $i++) {
|
||||
if (-not (Get-Process -Name 'CloudRestoreAS' -ErrorAction SilentlyContinue)) { break }
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $src -Destination $dest -Force
|
||||
Write-Ok "Binario instalado en $dest"
|
||||
|
||||
# --- Bootstrap de config\ -----------------------------------------------------------
|
||||
# El propio binario crea config\, .env y las carpetas de trabajo al arrancar. Se corre
|
||||
# una vez acotado por timeout para que el operador ya pueda editar config\.env.
|
||||
Write-Step 'Inicializando config\ (bootstrap)'
|
||||
Push-Location $Prefix
|
||||
try {
|
||||
$env:QT_QPA_PLATFORM = 'offscreen'
|
||||
$proc = Start-Process -FilePath $dest -ArgumentList '--headless' -PassThru -WindowStyle Hidden
|
||||
if (-not $proc.WaitForExit(20000)) {
|
||||
$proc | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
if (-not $sevenZipFound) {
|
||||
Write-Host "⚠️ 7-Zip no encontrado en ubicaciones estándar" -ForegroundColor Yellow
|
||||
Write-Host " Descarga 7-Zip desde: https://www.7-zip.org/" -ForegroundColor Yellow
|
||||
Write-Host " (Puedes configurar la ruta manualmente en la aplicación)" -ForegroundColor Cyan
|
||||
} finally {
|
||||
Remove-Item Env:\QT_QPA_PLATFORM -ErrorAction SilentlyContinue
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# Verificar ODBC Driver
|
||||
Write-Host ""
|
||||
Write-Host "3. Verificando ODBC Driver for SQL Server..." -ForegroundColor Yellow
|
||||
$odbcDrivers = Get-OdbcDriver | Where-Object {$_.Name -like "*SQL Server*"}
|
||||
if ($odbcDrivers) {
|
||||
Write-Host "✅ ODBC Driver encontrado:" -ForegroundColor Green
|
||||
$odbcDrivers | ForEach-Object { Write-Host " - $($_.Name)" -ForegroundColor Gray }
|
||||
$envPath = Join-Path $Prefix 'config\.env'
|
||||
if (Test-Path -LiteralPath $envPath) {
|
||||
Write-Ok 'config\.env creado.'
|
||||
} else {
|
||||
Write-Host "⚠️ ODBC Driver for SQL Server no encontrado" -ForegroundColor Yellow
|
||||
Write-Host " Descarga desde: https://aka.ms/downloadmsodbcsql" -ForegroundColor Yellow
|
||||
Write-Host " (Requerido para conectar con SQL Server)" -ForegroundColor Cyan
|
||||
Write-Warn 'config\.env se creará en la primera ejecución.'
|
||||
}
|
||||
|
||||
# Crear entorno virtual
|
||||
Write-Host ""
|
||||
Write-Host "4. Creando entorno virtual..." -ForegroundColor Yellow
|
||||
if (Test-Path "venv") {
|
||||
Write-Host " El entorno virtual ya existe, omitiendo..." -ForegroundColor Gray
|
||||
} else {
|
||||
python -m venv venv
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ Entorno virtual creado" -ForegroundColor Green
|
||||
# --- Siembra de credenciales del PANEL ----------------------------------------------
|
||||
# Fusión replace-or-append: respeta el resto de config\.env y es idempotente, así que
|
||||
# reinstalar no duplica claves ni pierde ajustes locales.
|
||||
function Merge-EnvFile {
|
||||
param([string]$Target, [hashtable]$Values)
|
||||
|
||||
$lines = if (Test-Path -LiteralPath $Target) {
|
||||
@(Get-Content -LiteralPath $Target -Encoding UTF8)
|
||||
} else { @() }
|
||||
|
||||
foreach ($key in $Values.Keys) {
|
||||
$line = "$key=$($Values[$key])"
|
||||
$idx = -1
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
if ($lines[$i] -match "^\s*$([regex]::Escape($key))\s*=") { $idx = $i; break }
|
||||
}
|
||||
if ($idx -ge 0) { $lines[$idx] = $line } else { $lines += $line }
|
||||
}
|
||||
|
||||
# Se escribe SIN BOM a propósito. `Set-Content -Encoding UTF8` en PowerShell 5.1 agrega BOM
|
||||
# (EF BB BF), y python-dotenv abre el archivo con encoding utf-8 (no utf-8-sig), así que el
|
||||
# BOM se pega a la primera línea. Si esa primera línea es una clave —lo que pasa cuando el
|
||||
# bootstrap no alcanzó a crear config\.env y este archivo se genera desde cero— la clave
|
||||
# queda ilegible para el agente: se instalaría sin conectarse al panel, con toda la
|
||||
# apariencia de un error de captura.
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllLines($Target, [string[]]$lines, $utf8NoBom)
|
||||
}
|
||||
|
||||
if ($PanelEnvFile) {
|
||||
if (-not (Test-Path -LiteralPath $PanelEnvFile)) {
|
||||
throw "No existe el archivo indicado en -PanelEnvFile: $PanelEnvFile"
|
||||
}
|
||||
Write-Step 'Sembrando credenciales del PANEL en config\.env'
|
||||
$allowed = @(
|
||||
'CLOUDRESTORE_PANEL_API_URL',
|
||||
'CLOUDRESTORE_PANEL_API_TOKEN',
|
||||
'CLOUDRESTORE_PANEL_INSTANCE_KEY',
|
||||
'CLOUDRESTORE_PANEL_VERIFY_SSL'
|
||||
)
|
||||
$values = @{}
|
||||
foreach ($raw in Get-Content -LiteralPath $PanelEnvFile -Encoding UTF8) {
|
||||
$line = $raw.Trim()
|
||||
if (-not $line -or $line.StartsWith('#')) { continue }
|
||||
$eq = $line.IndexOf('=')
|
||||
if ($eq -lt 1) { continue }
|
||||
$key = $line.Substring(0, $eq).Trim()
|
||||
# Lista blanca: el archivo viene de la red, no debe poder inyectar otras claves.
|
||||
if ($allowed -contains $key) {
|
||||
$values[$key] = $line.Substring($eq + 1).Trim()
|
||||
}
|
||||
}
|
||||
if ($values.Count -gt 0) {
|
||||
Merge-EnvFile -Target $envPath -Values $values
|
||||
Write-Ok "$($values.Count) clave(s) del PANEL escritas en config\.env"
|
||||
} else {
|
||||
Write-Host "❌ Error creando entorno virtual" -ForegroundColor Red
|
||||
Read-Host "Presiona Enter para salir..."
|
||||
exit 1
|
||||
Write-Warn 'El archivo -PanelEnvFile no traía claves CLOUDRESTORE_PANEL_* válidas.'
|
||||
}
|
||||
# El archivo trae el token en claro: se borra en cuanto se consumió.
|
||||
Remove-Item -LiteralPath $PanelEnvFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# --- Arranque automático ------------------------------------------------------------
|
||||
switch ($Mode) {
|
||||
'service' {
|
||||
Write-Step 'Registrando tarea programada ONSTART (SYSTEM)'
|
||||
# Sin NSSM: una tarea ONSTART como SYSTEM cubre el 24/7 headless con lo que ya
|
||||
# trae el SO. QT_QPA_PLATFORM=offscreen porque SYSTEM no tiene sesión gráfica.
|
||||
$action = New-ScheduledTaskAction -Execute $dest `
|
||||
-Argument '--start-engine --headless' -WorkingDirectory $Prefix
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount `
|
||||
-RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-ExecutionTimeLimit ([TimeSpan]::Zero)
|
||||
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
||||
-Principal $principal -Settings $settings -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Write-Ok "Tarea '$TaskName' registrada y arrancada."
|
||||
Write-Host " Estado : Get-ScheduledTask -TaskName $TaskName"
|
||||
Write-Host " Logs : $Prefix\config\logs"
|
||||
}
|
||||
'desktop' {
|
||||
Write-Host 'Modo escritorio: la app registra su autostart ONLOGON al iniciarse.'
|
||||
Write-Host "Ejecuta '$dest' en tu sesión."
|
||||
}
|
||||
'none' {
|
||||
Write-Host 'Instalación sin arranque automático.'
|
||||
Write-Host "Ejecuta: `"$dest`" --start-engine --headless"
|
||||
}
|
||||
}
|
||||
|
||||
# Activar entorno virtual
|
||||
Write-Host ""
|
||||
Write-Host "5. Instalando dependencias..." -ForegroundColor Yellow
|
||||
& ".\venv\Scripts\python.exe" -m pip install --upgrade pip
|
||||
& ".\venv\Scripts\pip.exe" install -r requirements.txt
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ Dependencias instaladas" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Error instalando dependencias" -ForegroundColor Red
|
||||
Read-Host "Presiona Enter para salir..."
|
||||
exit 1
|
||||
Write-Host ''
|
||||
Write-Host "Siguiente paso: revisa $envPath (CLOUDRESTORE_PANEL_*) y reinicia."
|
||||
if ($Mode -eq 'service') {
|
||||
Write-Host " Tras editar: Stop-ScheduledTask -TaskName $TaskName; Start-ScheduledTask -TaskName $TaskName"
|
||||
}
|
||||
|
||||
# Crear directorios necesarios
|
||||
Write-Host ""
|
||||
Write-Host "6. Creando directorios..." -ForegroundColor Yellow
|
||||
$dirs = @("data", "logs")
|
||||
foreach ($dir in $dirs) {
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir | Out-Null
|
||||
Write-Host "✅ Creado: $dir" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " $dir ya existe" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
# Resumen
|
||||
Write-Host ""
|
||||
Write-Host "===============================================" -ForegroundColor Green
|
||||
Write-Host "Instalación Completada" -ForegroundColor Green
|
||||
Write-Host "===============================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Para ejecutar la aplicación:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Activa el entorno virtual:" -ForegroundColor White
|
||||
Write-Host " .\venv\Scripts\Activate.ps1" -ForegroundColor Yellow
|
||||
Write-Host " 2. Ejecuta la aplicación:" -ForegroundColor White
|
||||
Write-Host " python runner.py" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "O ejecuta directamente:" -ForegroundColor Cyan
|
||||
Write-Host " .\venv\Scripts\python.exe runner.py" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "Lee el README.md para configuración completa." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Read-Host "Presiona Enter para salir..."
|
||||
Write-Host 'Listo.'
|
||||
|
||||
Reference in New Issue
Block a user