# ============================================================================ # Elaborado por Bryan Ramirez para Banco Promerica # ---------------------------------------------------------------------------- # Sync-WebConfig.ps1 - Replica bidireccional semi-realtime de CARPETAS # ENTERAS entre 2 servidores (10.9.6.32 <-> 10.9.6.33). # # Replica TODO el contenido de cada carpeta de $AppPaths: subcarpetas, # archivos, atributos, timestamps y permisos NTFS/ACL + propietario. # Cuando un archivo cambia de un lado, se propaga al otro partiendo de la # version MAS RECIENTE. Cuando un archivo se BORRA de un lado, se borra del # otro (espejo exacto bidireccional). # # EL MISMO ARCHIVO va en LOS DOS servidores. El script detecta solo en cual # esta corriendo por su IP local y deduce el peer. No hay que editar nada # salvo la lista $AppPaths. # # Como distingue "nuevo" de "borrado" (sin falsos borrados): # - Guarda un BASELINE por carpeta (lista de archivos que existian tras el # ultimo sync, en C:\Supervisor\state). # - Si un archivo estaba en el baseline y ya no esta en un lado -> fue # BORRADO -> se borra tambien en el otro. # - Si un archivo NO estaba en el baseline y aparece en un lado -> es # NUEVO -> se copia al otro. Asi nunca borra algo recien creado. # Anti-ping-pong: # - robocopy /XO solo copia si el origen es mas nuevo; tras copiar conserva # el timestamp, asi la direccion inversa lo omite. # ============================================================================ $ErrorActionPreference = 'Stop' # ----------------------------- CONFIG --------------------------------------- $NodeA = '10.9.6.32' $NodeB = '10.9.6.33' # Carpetas (rutas ABSOLUTAS con su letra de unidad) a replicar COMPLETAS. # Una por linea. Soporta espacios, subcarpetas y varias unidades (C:, E:, ...). # El peer se alcanza por el admin-share de esa misma unidad (C$, E$, ...). $AppPaths = @( 'E:\Produccion' 'E:\Fotos_Padron' 'C:\inetpub\wwwroot' ) $LogFile = 'C:\Supervisor\logs\webconfig_sync.log' $StateDir = 'C:\Supervisor\state' # baseline por carpeta (deteccion de borrados) $ReconcileSeconds = 15 # red de seguridad (reconcilia aunque no haya evento) $DebounceMs = 1000 # esperar a que termine de escribirse el archivo # Robocopy para el paso de copia (adds + gana el mas nuevo), SIN borrar (eso lo # maneja el baseline). /E arbol completo | /XO solo si origen mas nuevo | # /COPY:DATSO datos+atributos+timestamps+seguridad(ACL)+owner | /DCOPY:DAT carpetas | # /B modo backup (privilegio admin para ACL/owner en archivos restringidos) | # /R:2 /W:2 reintentos | /XJ ignora junctions | resto silencioso. $RoboCommon = @('/E','/XO','/COPY:DATSO','/DCOPY:DAT','/B','/R:2','/W:2','/XJ', '/NJH','/NJS','/NDL','/NP','/NFL') # ---------------------------------------------------------------------------- # --- Quien soy / quien es el peer (por IP local) --- $localIPs = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -Expand IPAddress) if ($localIPs -contains $NodeA) { $LocalIP = $NodeA; $Peer = $NodeB } elseif ($localIPs -contains $NodeB) { $LocalIP = $NodeB; $Peer = $NodeA } else { throw "Este host no tiene $NodeA ni $NodeB. Revisa NodeA/NodeB en el CONFIG." } # --- Carpetas de trabajo / estado --- foreach ($d in @((Split-Path $LogFile), $StateDir)) { if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null } } # --- Construye la lista de trabajos (una carpeta por entrada) --- $jobs = foreach ($ap in $AppPaths) { $full = $ap.TrimEnd('\') $qualifier = Split-Path -Qualifier $full # "E:" / "C:" $driveLetter = $qualifier.TrimEnd(':') # "E" / "C" $rest = $full.Substring($qualifier.Length).TrimStart('\') # "Produccion" [pscustomobject]@{ AppPath = $full LocalDir = $full RemoteDir = "\\$Peer\$driveLetter`$\$rest" # \\peer\E$\Produccion ShareRoot = "\\$Peer\$driveLetter`$" # \\peer\E$ (test de alcance) StateFile = Join-Path $StateDir (($full -replace '[^A-Za-z0-9]','_') + '.txt') } } # --- Log --- function Log([string]$m){ $line = "[{0}] [{1}->{2}] {3}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $LocalIP, $Peer, $m Add-Content -LiteralPath $LogFile -Value $line } # Conjunto de rutas RELATIVAS de archivos bajo $dir (recursivo). function Get-RelSet([string]$dir){ $base = $dir.TrimEnd('\') + '\' $set = @{} Get-ChildItem -LiteralPath $dir -Recurse -File -Force -ErrorAction SilentlyContinue | ForEach-Object { $set[$_.FullName.Substring($base.Length)] = $true } return $set } # Serializa el sync dentro de este proceso (watcher + timer no se pisan) $mutex = New-Object System.Threading.Mutex($false, 'Global\WebConfigSyncReconcile') function Invoke-Robo($src,$dst){ $null = & robocopy $src $dst @RoboCommon return $LASTEXITCODE # <8 = ok (0 nada,1 copio,2 extra,3 ambos); >=8 = error } function Sync-Folder($job, [string]$reason){ if (-not $mutex.WaitOne(30000)) { return } try { if (-not (Test-Path -LiteralPath $job.ShareRoot)) { Log "WARN [$($job.AppPath)] peer no accesible: $($job.ShareRoot) (reintento en ${ReconcileSeconds}s)" return } if (-not (Test-Path -LiteralPath $job.LocalDir)) { Log "WARN [$($job.AppPath)] carpeta local no existe: $($job.LocalDir)" return } $L = Get-RelSet $job.LocalDir $R = Get-RelSet $job.RemoteDir # --- Paso de BORRADOS (via baseline) --- $baselineExists = Test-Path -LiteralPath $job.StateFile $dels = 0 if ($baselineExists) { $baseline = @(Get-Content -LiteralPath $job.StateFile -ErrorAction SilentlyContinue) foreach ($rel in $baseline) { if ([string]::IsNullOrEmpty($rel)) { continue } $inL = $L.ContainsKey($rel); $inR = $R.ContainsKey($rel) if ($inL -and -not $inR) { # borrado en el peer -> borrar local Remove-Item -LiteralPath (Join-Path $job.LocalDir $rel) -Force -ErrorAction SilentlyContinue $L.Remove($rel); $dels++ Log "DEL local (borrado en peer): $rel" } elseif ($inR -and -not $inL) { # borrado en local -> borrar peer Remove-Item -LiteralPath (Join-Path $job.RemoteDir $rel) -Force -ErrorAction SilentlyContinue $R.Remove($rel); $dels++ Log "DEL peer (borrado en local): $rel" } } } # --- Paso de COPIA (adds + gana el mas nuevo), no borra --- $cPush = Invoke-Robo $job.LocalDir $job.RemoteDir # local -> peer $cPull = Invoke-Robo $job.RemoteDir $job.LocalDir # peer -> local $err = ($cPush -ge 8) -or ($cPull -ge 8) if ($err) { Log "ERROR [$($job.AppPath)] robocopy push=$cPush pull=$cPull ($reason)" } # --- Actualiza baseline = archivos presentes ahora (tras copiar quedan iguales) --- try { (Get-RelSet $job.LocalDir).Keys | Set-Content -LiteralPath $job.StateFile -Encoding UTF8 } catch { Log "WARN [$($job.AppPath)] no se pudo guardar baseline: $($_.Exception.Message)" } $copied = ((($cPush -band 1) -ne 0) -or (($cPull -band 1) -ne 0)) if ($copied -or $dels -gt 0) { Log "SYNC [$($job.AppPath)] push=$cPush pull=$cPull borrados=$dels ($reason)" } } catch { Log "ERROR [$($job.AppPath)] ($reason): $($_.Exception.Message)" } finally{ $mutex.ReleaseMutex() } } # --- Instancia unica: esta (la mas nueva) gana; termina cualquier otra copia viva --- try { Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -ne $PID -and $_.CommandLine -like '*Sync-WebConfig.ps1*' } | ForEach-Object { Log "Instancia previa detectada (PID $($_.ProcessId)) -> la termino (instancia unica)" Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } } catch { } Log "==== Sync iniciado ==== carpetas=$($jobs.Count) Local=$LocalIP Peer=$Peer" foreach ($job in $jobs) { Log " carpeta: $($job.LocalDir) <-> $($job.RemoteDir)" } foreach ($job in $jobs) { Sync-Folder $job 'startup' } # --- Watchers: uno por carpeta local, RECURSIVO (todo el arbol) --- $pending = [hashtable]::Synchronized(@{}) for ($i = 0; $i -lt $jobs.Count; $i++) { $pending[$i] = $false } $sb = { $Event.MessageData.Set[$Event.MessageData.Key] = $true } $watchers = @() for ($i = 0; $i -lt $jobs.Count; $i++) { $job = $jobs[$i] if (-not (Test-Path -LiteralPath $job.LocalDir)) { Log "WARN [$($job.AppPath)] carpeta local no existe; watcher omitido (el timer igual la revisa): $($job.LocalDir)" continue } $fsw = New-Object System.IO.FileSystemWatcher $job.LocalDir $fsw.NotifyFilter = [System.IO.NotifyFilters]'LastWrite, Size, FileName, DirectoryName, CreationTime' $fsw.IncludeSubdirectories = $true $fsw.EnableRaisingEvents = $true $md = [pscustomobject]@{ Set = $pending; Key = $i } Register-ObjectEvent $fsw Changed -MessageData $md -Action $sb | Out-Null Register-ObjectEvent $fsw Created -MessageData $md -Action $sb | Out-Null Register-ObjectEvent $fsw Renamed -MessageData $md -Action $sb | Out-Null Register-ObjectEvent $fsw Deleted -MessageData $md -Action $sb | Out-Null # borrado tambien dispara sync $watchers += $fsw } $lastTimer = Get-Date while ($true) { Start-Sleep -Milliseconds 400 $hot = @(0..($jobs.Count - 1) | Where-Object { $pending[$_] }) if ($hot.Count -gt 0) { Start-Sleep -Milliseconds $DebounceMs # dejar que termine de escribirse foreach ($k in $hot) { $pending[$k] = $false Sync-Folder $jobs[$k] 'fswatch' } $lastTimer = Get-Date } elseif (((Get-Date) - $lastTimer).TotalSeconds -ge $ReconcileSeconds) { foreach ($job in $jobs) { Sync-Folder $job 'timer' } $lastTimer = Get-Date } }