param( [Parameter(Mandatory = $true)] [string]$Version, [Parameter(Mandatory = $true)] [string]$ZipPath, [string]$ReportsZipPath = "", [Parameter(Mandatory = $true)] [string]$TargetId, [string]$TargetsPath = "" ) $ErrorActionPreference = "Stop" function Write-Step { param([string]$Message) Write-Host "[INFO] $Message" } function Write-Success { param([string]$Message) Write-Host "[SUCCESS] $Message" } function Get-ToolPath { param( [Parameter(Mandatory = $true)] [string]$FileName, [string[]]$FallbackPaths = @() ) $localPath = Join-Path $PSScriptRoot $FileName if (Test-Path $localPath) { return $localPath } foreach ($path in $FallbackPaths) { if (Test-Path $path) { return $path } } $command = Get-Command $FileName -ErrorAction SilentlyContinue if ($command) { return $command.Source } throw "No se encontro $FileName. Copialo junto a la app o instalalo con PuTTY." } function Get-ZipEntryNames { param( [Parameter(Mandatory = $true)] [string]$Path ) Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [System.IO.Compression.ZipFile]::OpenRead($Path) try { return @( $zip.Entries | Where-Object { -not [string]::IsNullOrWhiteSpace($_.FullName) } | ForEach-Object { $_.FullName.Replace("\", "/") } | Where-Object { $_ -and $_ -notmatch "^__MACOSX/" -and $_ -notmatch "(^|/)\.DS_Store$" } ) } finally { $zip.Dispose() } } function ConvertTo-ShellArg { param( [Parameter(Mandatory = $true)] [string]$Value ) $singleQuote = [string][char]39 return $singleQuote + $Value.Replace($singleQuote, "$singleQuote\$singleQuote$singleQuote") + $singleQuote } function Invoke-Plink { param( [Parameter(Mandatory = $true)] [string]$Command ) & $script:Plink -ssh "$($script:Target.user)@$($script:Target.host)" -P $script:Target.port -pw $script:Target.password -batch $Command 2>&1 } if ([string]::IsNullOrWhiteSpace($TargetsPath)) { $TargetsPath = Join-Path $PSScriptRoot "targets.json" } if (-not (Test-Path $TargetsPath)) { throw "No se encontro el archivo de destinos: $TargetsPath" } if (-not (Test-Path $ZipPath)) { throw "No se encontro el ZIP principal: $ZipPath" } $targetsConfig = Get-Content -Raw -Path $TargetsPath | ConvertFrom-Json $targetMatches = @($targetsConfig.targets | Where-Object { $_.id -eq $TargetId }) if ($targetMatches.Count -eq 0) { throw "No existe el destino seleccionado: $TargetId" } $script:Target = $targetMatches[0] $script:Plink = Get-ToolPath -FileName "plink.exe" -FallbackPaths @( "C:\Program Files\PuTTY\plink.exe", "C:\Program Files (x86)\PuTTY\plink.exe" ) $pscp = Get-ToolPath -FileName "pscp.exe" -FallbackPaths @( "C:\Program Files\PuTTY\pscp.exe", "C:\Program Files (x86)\PuTTY\pscp.exe" ) $zipFileName = Split-Path -Leaf $ZipPath $reportesFolderName = "reportes_subidos_jasper" $hasReportsZipPath = -not [string]::IsNullOrWhiteSpace($ReportsZipPath) if ($hasReportsZipPath -and -not (Test-Path $ReportsZipPath)) { throw "No se encontro el ZIP de reportes: $ReportsZipPath" } Write-Step "Version indicada: $Version" Write-Step "Destino: $($Target.name) ($($Target.host):$($Target.port))" Write-Step "Validando estructura del ZIP principal..." $zipEntryNames = Get-ZipEntryNames -Path $ZipPath if ($zipEntryNames.Count -eq 0) { throw "El ZIP principal esta vacio: $ZipPath" } $topLevelNames = @( $zipEntryNames | ForEach-Object { ($_ -split "/", 2)[0] } | Where-Object { $_ } | Sort-Object -Unique ) $topLevelFiles = @( $zipEntryNames | Where-Object { ($_ -notmatch "/") -and (-not $_.EndsWith("/")) } ) $zipRootPrefix = $null if (($topLevelNames.Count -eq 1) -and ($topLevelFiles.Count -eq 0)) { $zipRootPrefix = $topLevelNames[0] Write-Step "El ZIP contiene una carpeta raiz ($zipRootPrefix). Se desplegara su contenido." } $deployEntryNames = $zipEntryNames if ($zipRootPrefix) { $rootPrefixPattern = "^" + [regex]::Escape($zipRootPrefix.TrimEnd("/")) + "/" $deployEntryNames = @( $zipEntryNames | Where-Object { $_ -match $rootPrefixPattern } | ForEach-Object { $_ -replace $rootPrefixPattern, "" } | Where-Object { $_ } ) } $deployTopLevelFolders = @( $deployEntryNames | Where-Object { $_ -match "/" } | ForEach-Object { ($_ -split "/", 2)[0] } | Where-Object { $_ } | Sort-Object -Unique ) if ($deployTopLevelFolders.Count -le 1) { throw "El contenido a desplegar debe tener mas de una carpeta en el nivel padre." } $hasReportesFolder = @( $deployEntryNames | Where-Object { $_ -eq "$reportesFolderName/" -or $_ -like "$reportesFolderName/*" } ).Count -gt 0 $needsReportesZip = -not $hasReportesFolder $reportesZipExtractMode = "direct" $reportesZipName = "" if ($needsReportesZip) { if (-not $hasReportsZipPath) { Write-Step "El ZIP no incluye $reportesFolderName y no se selecciono ZIP auxiliar. Se continua sin reportes." } else { $reportesZipName = Split-Path -Leaf $ReportsZipPath $reportesEntryNames = Get-ZipEntryNames -Path $ReportsZipPath if ($reportesEntryNames.Count -eq 0) { throw "El ZIP de reportes esta vacio: $ReportsZipPath" } $reportesZipHasFolder = @( $reportesEntryNames | Where-Object { $_ -eq "$reportesFolderName/" -or $_ -like "$reportesFolderName/*" } ).Count -gt 0 if (-not $reportesZipHasFolder) { $reportesZipExtractMode = "into-folder" Write-Step "El ZIP de reportes no contiene carpeta raiz. Se extraera dentro de $reportesFolderName." } } } $remoteUploadPath = $Target.remoteUploadPath $remoteAppPath = $Target.remoteAppPath $backupDir = $Target.backupDir $jasperstarterPath = "$remoteAppPath/_lib/libraries/grp/composer/vendor/geekcom/phpjasper/bin/jasperstarter/bin/jasperstarter" $remoteTempPath = "$remoteUploadPath/gt_nova_deploy_tmp_$Version" $remoteZipPath = "$remoteUploadPath/$zipFileName" $remoteReportesZipPath = if ($reportesZipName) { "$remoteUploadPath/$reportesZipName" } else { "" } $remoteExtractSource = if ($zipRootPrefix) { "$remoteTempPath/$zipRootPrefix" } else { $remoteTempPath } $remoteAppArg = ConvertTo-ShellArg $remoteAppPath $remoteTempArg = ConvertTo-ShellArg $remoteTempPath $remoteZipArg = ConvertTo-ShellArg $remoteZipPath $remoteExtractSourceArg = ConvertTo-ShellArg $remoteExtractSource $remoteReportesFolderArg = ConvertTo-ShellArg "$remoteAppPath/$reportesFolderName" $remoteReportesZipArg = if ($remoteReportesZipPath) { ConvertTo-ShellArg $remoteReportesZipPath } else { "" } $jasperstarterArg = ConvertTo-ShellArg $jasperstarterPath Write-Step "Probando conexion SSH..." $connectionCheck = Invoke-Plink -Command "echo SSH_OK" if ($connectionCheck -notmatch "SSH_OK") { throw "No se pudo conectar por SSH. Salida: $connectionCheck" } Write-Success "Conexion SSH OK." $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $backupFileName = "backup_gt_nova_${Version}_$timestamp.tar.gz" $backupCommand = "mkdir -p $(ConvertTo-ShellArg $backupDir) && tar -czf $(ConvertTo-ShellArg "$backupDir/$backupFileName") -C /var/www/html $(Split-Path -Leaf $remoteAppPath) && echo BACKUP_OK" Write-Step "Creando backup remoto..." $backupOutput = Invoke-Plink -Command $backupCommand if ($backupOutput -notmatch "BACKUP_OK") { throw "Error al crear backup remoto. Salida: $backupOutput" } Write-Success "Backup creado: $backupDir/$backupFileName" Write-Step "Subiendo ZIP principal..." & $pscp -P $Target.port -pw $Target.password $ZipPath "$($Target.user)@$($Target.host):$remoteUploadPath/" 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -ne 0) { throw "Error al subir el ZIP principal." } Write-Success "ZIP principal subido." $shouldUploadReportsZip = $needsReportesZip -and $hasReportsZipPath if ($shouldUploadReportsZip) { Write-Step "Subiendo ZIP auxiliar de reportes..." & $pscp -P $Target.port -pw $Target.password $ReportsZipPath "$($Target.user)@$($Target.host):$remoteUploadPath/" 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -ne 0) { throw "Error al subir el ZIP de reportes." } Write-Success "ZIP de reportes subido." } $cmdReportes = ":" if ($shouldUploadReportsZip -and $reportesZipExtractMode -eq "direct") { $cmdReportes = "sudo unzip -oq $remoteReportesZipArg -d $remoteAppArg" } elseif ($shouldUploadReportsZip -and $reportesZipExtractMode -eq "into-folder") { $cmdReportes = @" sudo mkdir -p $remoteReportesFolderArg sudo unzip -oq $remoteReportesZipArg -d $remoteReportesFolderArg "@ } Write-Step "Descomprimiendo y reemplazando aplicacion..." $deployCommand = @" set -e sudo rm -rf $remoteTempArg sudo mkdir -p $remoteTempArg $remoteAppArg sudo unzip -q $remoteZipArg -d $remoteTempArg sudo find $remoteAppArg -mindepth 1 -maxdepth 1 -exec rm -rf {} + sudo cp -a $remoteExtractSourceArg/. $remoteAppArg/ $cmdReportes sudo rm -rf $remoteTempArg echo UNZIP_OK "@ $deployOutput = Invoke-Plink -Command $deployCommand if ($deployOutput -notmatch "UNZIP_OK") { throw "Error al descomprimir o reemplazar aplicacion. Salida: $deployOutput" } Write-Success "Aplicacion reemplazada." $serviceRestartLines = @() foreach ($service in @($Target.services)) { if (-not [string]::IsNullOrWhiteSpace($service)) { $serviceRestartLines += "sudo systemctl restart $(ConvertTo-ShellArg $service)" } } $cleanupReportes = if ($shouldUploadReportsZip) { "sudo rm -f $remoteReportesZipArg" } else { ":" } $fixJasperstarterPermissions = ":" if ($Target.host -eq "4.246.183.98") { $fixJasperstarterPermissions = @" if [ -f $jasperstarterArg ]; then sudo chmod 755 $jasperstarterArg ls -l $jasperstarterArg else echo JASPERSTARTER_NOT_FOUND exit 1 fi "@ } $postDeployCommand = @" set -e sudo chown -R www-data:www-data $remoteAppArg sudo find $remoteAppArg -type d -exec chmod 755 {} \; sudo find $remoteAppArg -type f -exec chmod 644 {} \; $fixJasperstarterPermissions $($serviceRestartLines -join "`n") sudo rm -f $remoteZipArg $cleanupReportes echo DEPLOY_OK "@ Write-Step "Aplicando permisos, reiniciando servicios y limpiando temporales..." $postDeployOutput = Invoke-Plink -Command $postDeployCommand if ($postDeployOutput -notmatch "DEPLOY_OK") { throw "El deploy reemplazo archivos, pero fallo el post-deploy. Salida: $postDeployOutput" } if ($Target.host -eq "4.246.183.98") { if ($postDeployOutput -notmatch "jasperstarter") { throw "El deploy finalizo, pero no se pudo verificar el permiso de jasperstarter. Salida: $postDeployOutput" } Write-Success "Permiso 755 aplicado a jasperstarter." } Write-Success "Deploy finalizado correctamente para version $Version en $($Target.name)."