# Sync-AD2-NAS.ps1 (formerly Sync-FromNAS.ps1) # Bidirectional sync between AD2 and NAS (D2TESTNAS) # # PULL (NAS -> AD2): Test results (LOGS/*.DAT, Reports/*.TXT) -> Database import # PUSH (AD2 -> NAS): Software updates (ProdSW/*, TODO.BAT) -> DOS machines # # Run: powershell -ExecutionPolicy Bypass -File C:\Shares\test\scripts\Sync-FromNAS.ps1 # Scheduled: Every 15 minutes via Windows Task Scheduler param( [switch]$DryRun, # Show what would be done without doing it [switch]$Verbose, # Extra output [int]$MaxAgeMinutes = 1440 # Default: files from last 24 hours (was 60 min, too aggressive) ) # ============================================================================ # Configuration # ============================================================================ $NAS_IP = "192.168.0.9" $NAS_USER = "root" $NAS_PASSWORD = "Paper123!@#-nas" $NAS_HOSTKEY = "SHA256:5CVIPlqjLPxO8n48PKLAP99nE6XkEBAjTkaYmJAeOdA" $NAS_DATA_PATH = "/data/test" $AD2_TEST_PATH = "C:\Shares\test" $AD2_HISTLOGS_PATH = "C:\Shares\test\Ate\HISTLOGS" $SSH = "C:\Program Files\OpenSSH\ssh.exe" $SCP = "C:\Program Files\OpenSSH\scp.exe" $SSH_KEY = "C:\Users\sysadmin\.ssh\id_ed25519" $LOG_FILE = "C:\Shares\test\scripts\sync-from-nas.log" $STATUS_FILE = "C:\Shares\test\_SYNC_STATUS.txt" $LOG_TYPES = @("5BLOG", "7BLOG", "8BLOG", "DSCLOG", "SCTLOG", "VASLOG", "PWRLOG", "HVLOG") # Database import configuration $IMPORT_SCRIPT = "C:\Shares\testdatadb\database\import.js" $NODE_PATH = "node" # ============================================================================ # Functions # ============================================================================ function Write-Log { param([string]$Message) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logLine = "$timestamp : $Message" Add-Content -Path $LOG_FILE -Value $logLine if ($Verbose) { Write-Host $logLine } } function Invoke-NASCommand { param([string]$Command) $result = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" $Command 2>&1 return $result } function Get-NASFileList { param( [string]$FindCommand, [string]$ListLabel ) # Generate file list on NAS side, write to temp file, pull via SCP # Key fix: use *> $null to discard all PowerShell output streams, preventing # the stdout buffer deadlock that occurs with & $SSH ... 2>&1 $remoteTemp = "/tmp/nas-file-list-${ListLabel}.txt" # Step 1: Run find on NAS, redirect output to temp file on NAS # We discard ALL PowerShell output (*> $null) - we only need the side effect & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" "$FindCommand > $remoteTemp 2>/dev/null" *> $null # Step 2: Pull the list file via SCP $localTemp = "$env:TEMP\nas-file-list-${ListLabel}-$(Get-Date -Format 'yyyyMMddHHmmss').txt" & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${remoteTemp}" "$localTemp" *> $null # Step 3: Read locally and clean up $fileList = Get-Content $localTemp -ErrorAction SilentlyContinue | Where-Object { $_.Trim() -ne '' } Remove-Item $localTemp -ErrorAction SilentlyContinue # Step 4: Clean up remote temp file (small output, safe with Invoke-NASCommand) Invoke-NASCommand "rm -f $remoteTemp" | Out-Null return $fileList } function Copy-FromNAS { param( [string]$RemotePath, [string]$LocalPath ) # Ensure local directory exists $localDir = Split-Path -Parent $LocalPath if (-not (Test-Path $localDir)) { New-Item -ItemType Directory -Path $localDir -Force | Out-Null } # Escape spaces and special chars with backslashes for SCP remote path $escapedPath = $RemotePath -replace '([ ()\[\]{}])', '\$1' $result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${escapedPath}" "$LocalPath" 2>&1 if ($LASTEXITCODE -ne 0) { $errorMsg = $result | Out-String Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg" } return $LASTEXITCODE -eq 0 } function Rename-OnNAS { param([string]$RemotePath) Invoke-NASCommand "mv '$RemotePath' '${RemotePath}.synced'" | Out-Null } function Copy-ToNAS { param( [string]$LocalPath, [string]$RemotePath ) # Ensure remote directory exists via SSH mkdir -p $remoteDir = ($RemotePath -replace '[^/]+$', '').TrimEnd('/') Invoke-NASCommand "mkdir -p '$remoteDir'" | Out-Null # Escape spaces and special chars with backslashes for SCP remote path $escapedPath = $RemotePath -replace '([ ()\[\]{}])', '\$1' $result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${escapedPath}" 2>&1 if ($LASTEXITCODE -ne 0) { $errorMsg = $result | Out-String Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg" } return $LASTEXITCODE -eq 0 } function Test-8dot3Name { param([string]$Name) # Returns $true if filename is 8.3 compatible (no spaces, name<=8, ext<=3) if ($Name -match '[ ()\[\]{}]') { return $false } $parts = $Name -split '\.' if ($parts.Count -gt 2) { return $false } if ($parts[0].Length -gt 8 -or $parts[0].Length -eq 0) { return $false } if ($parts.Count -eq 2 -and $parts[1].Length -gt 3) { return $false } return $true } function Test-8dot3Path { param([string]$RelativePath) # Check every component of the path is 8.3 compatible $segments = $RelativePath -replace '\\', '/' -split '/' foreach ($seg in $segments) { if ($seg -eq '') { continue } if (-not (Test-8dot3Name $seg)) { return $false } } return $true } function Get-FileHash256 { param([string]$FilePath) if (Test-Path $FilePath) { return (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash } return $null } function Import-ToDatabase { param([string[]]$FilePaths) if ($FilePaths.Count -eq 0) { return } Write-Log "Importing $($FilePaths.Count) file(s) to database..." # Build argument list $importArgs = @("$IMPORT_SCRIPT", "--file") + $FilePaths try { $output = & $NODE_PATH $importArgs 2>&1 foreach ($line in $output) { Write-Log " [DB] $line" } Write-Log "Database import complete" } catch { Write-Log "ERROR: Database import failed: $_" } } # ============================================================================ # Main Script # ============================================================================ Write-Log "==========================================" Write-Log "Starting sync from NAS" Write-Log "Max age: $MaxAgeMinutes minutes" if ($DryRun) { Write-Log "DRY RUN - no changes will be made" } # Ensure .ssh directory exists for known_hosts $sshDir = "C:\Shares\test\scripts\.ssh" if (-not (Test-Path $sshDir)) { New-Item -ItemType Directory -Path $sshDir -Force | Out-Null Write-Log "Created .ssh directory: $sshDir" } $errorCount = 0 $syncedFiles = 0 $skippedFiles = 0 $syncedDatFiles = @() # Track DAT files for database import # Find all DAT files on NAS modified within the time window # Uses temp file approach to avoid SSH output buffer hang with large file counts Write-Log "Finding DAT files on NAS..." $findCommand = "find $NAS_DATA_PATH/TS-*/LOGS -name '*.DAT' -type f -mmin -$MaxAgeMinutes" $datFiles = Get-NASFileList -FindCommand $findCommand -ListLabel "dat" if (-not $datFiles -or $datFiles.Count -eq 0) { Write-Log "No new DAT files found on NAS" } else { Write-Log "Found $($datFiles.Count) DAT file(s) to process" foreach ($remoteFile in $datFiles) { $remoteFile = $remoteFile.Trim() if ([string]::IsNullOrWhiteSpace($remoteFile)) { continue } # Parse the path: /data/test/TS-XX/LOGS/7BLOG/file.DAT if ($remoteFile -match "/data/test/(TS-[^/]+)/LOGS/([^/]+)/(.+\.DAT)$") { $station = $Matches[1] $logType = $Matches[2] $fileName = $Matches[3] Write-Log "Processing: $station/$logType/$fileName" # Destination 1: Per-station folder (preserves structure) $stationDest = Join-Path $AD2_TEST_PATH "$station\LOGS\$logType\$fileName" # Destination 2: Aggregated HISTLOGS folder $histlogsDest = Join-Path $AD2_HISTLOGS_PATH "$logType\$fileName" if ($DryRun) { Write-Log " [DRY RUN] Would copy to: $stationDest" $syncedFiles++ } else { # Copy to station folder only (skip HISTLOGS to avoid duplicates) $success1 = Copy-FromNAS -RemotePath $remoteFile -LocalPath $stationDest if ($success1) { Write-Log " Copied to station folder" # Rename on NAS after successful sync (preserves file as .synced) Rename-OnNAS -RemotePath $remoteFile Write-Log " Renamed to .synced on NAS" # Track for database import $syncedDatFiles += $stationDest $syncedFiles++ } else { Write-Log " ERROR: Failed to copy from NAS" $errorCount++ } } } else { Write-Log " Skipping (unexpected path format): $remoteFile" $skippedFiles++ } } } # Find and sync TXT report files # Uses temp file approach to avoid SSH output buffer hang with large file counts Write-Log "Finding TXT reports on NAS..." $findReportsCommand = "find $NAS_DATA_PATH/TS-*/Reports -name '*.TXT' -type f -mmin -$MaxAgeMinutes" $txtFiles = Get-NASFileList -FindCommand $findReportsCommand -ListLabel "txt" if ($txtFiles -and $txtFiles.Count -gt 0) { Write-Log "Found $($txtFiles.Count) TXT report(s) to process" foreach ($remoteFile in $txtFiles) { $remoteFile = $remoteFile.Trim() if ([string]::IsNullOrWhiteSpace($remoteFile)) { continue } if ($remoteFile -match "/data/test/(TS-[^/]+)/Reports/(.+\.TXT)$") { $station = $Matches[1] $fileName = $Matches[2] Write-Log "Processing report: $station/$fileName" # Destination: Per-station Reports folder $reportDest = Join-Path $AD2_TEST_PATH "$station\Reports\$fileName" if ($DryRun) { Write-Log " [DRY RUN] Would copy to: $reportDest" $syncedFiles++ } else { $success = Copy-FromNAS -RemotePath $remoteFile -LocalPath $reportDest if ($success) { Write-Log " Copied report" Rename-OnNAS -RemotePath $remoteFile Write-Log " Renamed to .synced on NAS" $syncedFiles++ } else { Write-Log " ERROR: Failed to copy report" $errorCount++ } } } } } # ============================================================================ # Import synced DAT files to database # ============================================================================ if (-not $DryRun -and $syncedDatFiles.Count -gt 0) { Import-ToDatabase -FilePaths $syncedDatFiles } # ============================================================================ # PUSH: AD2 -> NAS (Software Updates for DOS Machines) # ============================================================================ Write-Log "--- AD2 to NAS Sync (Software Updates) ---" $pushedFiles = 0 # Sync COMMON/ProdSW (batch files for all stations) # AD2 uses _COMMON, NAS uses COMMON - handle both $commonSources = @( @{ Local = "$AD2_TEST_PATH\_COMMON\ProdSW"; Remote = "$NAS_DATA_PATH/COMMON/ProdSW" }, @{ Local = "$AD2_TEST_PATH\COMMON\ProdSW"; Remote = "$NAS_DATA_PATH/COMMON/ProdSW" } ) foreach ($source in $commonSources) { if (Test-Path $source.Local) { Write-Log "Syncing COMMON ProdSW from: $($source.Local)" $commonFiles = Get-ChildItem -Path $source.Local -File -ErrorAction SilentlyContinue foreach ($file in $commonFiles) { if (-not (Test-8dot3Name $file.Name)) { Write-Log " Skipping (non-8.3): $($file.Name)" $skippedFiles++ continue } $remotePath = "$($source.Remote)/$($file.Name)" if ($DryRun) { Write-Log " [DRY RUN] Would push: $($file.Name) -> $remotePath" $pushedFiles++ } else { $success = Copy-ToNAS -LocalPath $file.FullName -RemotePath $remotePath if ($success) { Write-Log " Pushed: $($file.Name)" $pushedFiles++ } else { Write-Log " ERROR: Failed to push $($file.Name)" $errorCount++ } } } } } # Sync Ate/ProdSW (shared ATE data folders - 5BDATA, 7BDATA, 8BDATA, DSCDATA, etc.) Write-Log "Syncing Ate/ProdSW data folders..." $ateProdSwPath = "$AD2_TEST_PATH\Ate\ProdSW" if (Test-Path $ateProdSwPath) { # Get all files recursively (including subdirectories like DSCDATA, 5BDATA, etc.) $ateFiles = Get-ChildItem -Path $ateProdSwPath -File -Recurse -ErrorAction SilentlyContinue foreach ($file in $ateFiles) { # Calculate relative path from Ate/ProdSW folder $relativePath = $file.FullName.Substring($ateProdSwPath.Length + 1).Replace('\', '/') if (-not (Test-8dot3Path $relativePath)) { Write-Log " Skipping (non-8.3): Ate/ProdSW/$relativePath" $skippedFiles++ continue } $remotePath = "$NAS_DATA_PATH/Ate/ProdSW/$relativePath" if ($DryRun) { Write-Log " [DRY RUN] Would push: Ate/ProdSW/$relativePath" $pushedFiles++ } else { $success = Copy-ToNAS -LocalPath $file.FullName -RemotePath $remotePath if ($success) { Write-Log " Pushed: Ate/ProdSW/$relativePath" $pushedFiles++ } else { Write-Log " ERROR: Failed to push Ate/ProdSW/$relativePath" $errorCount++ } } } } else { Write-Log " Ate/ProdSW not found: $ateProdSwPath" } # Sync UPDATE.BAT (root level utility) Write-Log "Syncing UPDATE.BAT..." $updateBatLocal = "$AD2_TEST_PATH\UPDATE.BAT" if (Test-Path $updateBatLocal) { $updateBatRemote = "$NAS_DATA_PATH/UPDATE.BAT" if ($DryRun) { Write-Log " [DRY RUN] Would push: UPDATE.BAT -> $updateBatRemote" $pushedFiles++ } else { $success = Copy-ToNAS -LocalPath $updateBatLocal -RemotePath $updateBatRemote if ($success) { Write-Log " Pushed: UPDATE.BAT" $pushedFiles++ } else { Write-Log " ERROR: Failed to push UPDATE.BAT" $errorCount++ } } } else { Write-Log " WARNING: UPDATE.BAT not found at $updateBatLocal" } # Sync DEPLOY.BAT (root level utility) Write-Log "Syncing DEPLOY.BAT..." $deployBatLocal = "$AD2_TEST_PATH\DEPLOY.BAT" if (Test-Path $deployBatLocal) { $deployBatRemote = "$NAS_DATA_PATH/DEPLOY.BAT" if ($DryRun) { Write-Log " [DRY RUN] Would push: DEPLOY.BAT -> $deployBatRemote" $pushedFiles++ } else { $success = Copy-ToNAS -LocalPath $deployBatLocal -RemotePath $deployBatRemote if ($success) { Write-Log " Pushed: DEPLOY.BAT" $pushedFiles++ } else { Write-Log " ERROR: Failed to push DEPLOY.BAT" $errorCount++ } } } else { Write-Log " WARNING: DEPLOY.BAT not found at $deployBatLocal" } # Sync per-station ProdSW folders Write-Log "Syncing station-specific ProdSW folders..." $stationFolders = Get-ChildItem -Path $AD2_TEST_PATH -Directory -Filter "TS-*" -ErrorAction SilentlyContinue foreach ($station in $stationFolders) { # Skip station folders with non-8.3 names (e.g. "TS-1 OBS") if (-not (Test-8dot3Name $station.Name)) { Write-Log " Skipping station (non-8.3 name): $($station.Name)" continue } $prodSwPath = Join-Path $station.FullName "ProdSW" if (Test-Path $prodSwPath) { # Get all files in ProdSW (including subdirectories) $prodSwFiles = Get-ChildItem -Path $prodSwPath -File -Recurse -ErrorAction SilentlyContinue foreach ($file in $prodSwFiles) { # Calculate relative path from ProdSW folder $relativePath = $file.FullName.Substring($prodSwPath.Length + 1).Replace('\', '/') if (-not (Test-8dot3Path $relativePath)) { Write-Log " Skipping (non-8.3): $($station.Name)/ProdSW/$relativePath" $skippedFiles++ continue } $remotePath = "$NAS_DATA_PATH/$($station.Name)/ProdSW/$relativePath" if ($DryRun) { Write-Log " [DRY RUN] Would push: $($station.Name)/ProdSW/$relativePath" $pushedFiles++ } else { $success = Copy-ToNAS -LocalPath $file.FullName -RemotePath $remotePath if ($success) { Write-Log " Pushed: $($station.Name)/ProdSW/$relativePath" $pushedFiles++ } else { Write-Log " ERROR: Failed to push $($station.Name)/ProdSW/$relativePath" $errorCount++ } } } } # Check for TODO.BAT (one-time task file) $todoBatPath = Join-Path $station.FullName "TODO.BAT" if (Test-Path $todoBatPath) { $remoteTodoPath = "$NAS_DATA_PATH/$($station.Name)/TODO.BAT" Write-Log "Found TODO.BAT for $($station.Name)" if ($DryRun) { Write-Log " [DRY RUN] Would push TODO.BAT -> $remoteTodoPath" $pushedFiles++ } else { $success = Copy-ToNAS -LocalPath $todoBatPath -RemotePath $remoteTodoPath if ($success) { Write-Log " Pushed TODO.BAT to NAS" # Remove from AD2 after successful push (one-shot mechanism) Remove-Item -Path $todoBatPath -Force Write-Log " Removed TODO.BAT from AD2 (pushed to NAS)" $pushedFiles++ } else { Write-Log " ERROR: Failed to push TODO.BAT" $errorCount++ } } } } Write-Log "AD2 to NAS sync: $pushedFiles file(s) pushed" # ============================================================================ # Update Status File # ============================================================================ $status = if ($errorCount -eq 0) { "OK" } else { "ERRORS" } $statusContent = @" AD2 <-> NAS Bidirectional Sync Status ====================================== Timestamp: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Status: $status PULL (NAS -> AD2 - Test Results): Files Pulled: $syncedFiles Files Skipped: $skippedFiles DAT Files Imported to DB: $($syncedDatFiles.Count) PUSH (AD2 -> NAS - Software Updates): Files Pushed: $pushedFiles Errors: $errorCount "@ Set-Content -Path $STATUS_FILE -Value $statusContent Write-Log "==========================================" Write-Log "Sync complete: PULL=$syncedFiles, PUSH=$pushedFiles, Errors=$errorCount" Write-Log "==========================================" # Exit with error code if there were failures if ($errorCount -gt 0) { exit 1 } else { exit 0 }