<# .SYNOPSIS Compares the last two versions of a specified file for a given company based on the manifest.json. .DESCRIPTION This script takes a company name and a file name as input, reads the manifest.json to find the last two backup versions of the file, extracts them, locates the specific files within the backups, and performs a comparison. The results are logged for audit control. .PARAMETER CompanyName The name of the company whose file you want to compare. .PARAMETER FileName The specific file within the company's directory to compare. .EXAMPLE .\Compare-NWABackup.ps1 -CompanyName "CompanyA" -FileName "DataFile.DAT" .NOTES Ensure that the manifest.json is correctly formatted and accessible. #> param ( [Parameter(Mandatory = $true)] [string]$CompanyName, [Parameter(Mandatory = $true)] [string]$FileName ) # ----------- Step 1: Define Variables ----------- # Assuming manifest.json is located in the backup root directory $backupRoot = "C:\Users\$env:USERNAME\Desktop\NWABackup" # Adjust as necessary $manifestFilePath = Join-Path -Path $backupRoot -ChildPath "manifest.json" # Define log file path $logDirectory = Join-Path -Path $backupRoot -ChildPath "Logs" $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $logFile = Join-Path -Path $logDirectory -ChildPath "$timestamp.log" # Define temporary extraction directories $tempExtractionRoot = Join-Path -Path $backupRoot -ChildPath "TempComparisons" $tempDirLatest = Join-Path -Path $tempExtractionRoot -ChildPath "$FileName\Latest_$timestamp" $tempDirPrevious = Join-Path -Path $tempExtractionRoot -ChildPath "$FileName\Previous_$timestamp" # ----------- Step 0: Import Logging Module ----------- $modulesPath = Join-Path -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ChildPath "" Import-Module (Join-Path -Path $modulesPath -ChildPath "Logging\Logging.psm1") -Force # Create log directory if it doesn't exist if (!(Test-Path -Path $logDirectory)) { try { New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null # Initialize Logging after creating the log directory Initialize-Logging -Path $logFile Write-Log -Message "Log directory created at ${logDirectory}" -Type "INFO" } catch { Write-Error "Failed to create log directory at ${logDirectory}: $_" exit 1 } } else { # Initialize Logging if log directory already exists Initialize-Logging -Path $logFile } # ----------- Step 2: Initialize Logging ----------- # Create log directory if it doesn't exist if (!(Test-Path -Path $logDirectory)) { try { New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null # Initialize Logging after creating the log directory Initialize-Logging -Path $logFile Write-Log -Message "Log directory created at ${logDirectory}" -Type "INFO" } catch { Write-Error "Failed to create log directory at ${logDirectory}: $_" exit 1 } } else { # Initialize Logging if log directory already exists Initialize-Logging -Path $logFile } # Log the start of the comparison process Write-Log -Message "=== NWABackup Comparison Process Started at $timestamp ===" -Type "INFO" # ----------- Step 3: Load the Manifest and Find the File ----------- if (!(Test-Path -Path $manifestFilePath)) { Write-Log -Message "Manifest file not found at $manifestFilePath." -Type "ERROR" exit 1 } try { $manifestContent = Get-Content -Path $manifestFilePath -Raw | ConvertFrom-Json Write-Log -Message "Manifest file loaded successfully." -Type "INFO" } catch { Write-Log -Message "Failed to load manifest file: $_" -Type "ERROR" exit 1 } # Check if the company exists in the manifest if (-not ($manifestContent.PSObject.Properties.Name -contains $CompanyName)) { Write-Log -Message "Company '$CompanyName' not found in the manifest." -Type "ERROR" exit 1 } # Navigate to the specific part for the given company $companyData = $manifestContent.$CompanyName $targetPart = $null $targetZipPaths = @() foreach ($partName in $companyData.PSObject.Properties.Name) { # Check if the part contains a 'DAT' entry if ($companyData.$partName.PSObject.Properties.Name -contains 'DAT') { # Retrieve the list of zip files for 'DAT' $datFiles = $companyData.$partName.DAT # Check if the part name matches the provided file name (excluding extension) $fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($FileName) if ($fileBaseName -eq $partName) { $targetPart = $partName $targetZipPaths = $datFiles break } } } if (-not $targetPart) { Write-Log -Message "File '$FileName' not found under any part for company '$CompanyName'." -Type "ERROR" exit 1 } Write-Log -Message "Found '$FileName' under part '$targetPart' for company '$CompanyName'." -Type "INFO" # Use the matching zip paths for comparison $backupZips = $targetZipPaths # ----------- Step 4: Check Number of Versions ----------- if ($backupZips.Count -lt 2) { Write-Log -Message "Only $($backupZips.Count) version(s) found for $CompanyName\$FileName. At least two versions are required for comparison." -Type "WARNING" exit 0 } # ----------- Step 5: Get Locations of the Two Prior Backups ----------- # Function to extract DateTime from zip file name function Get-ZipTimestamp { param ( [string]$ZipPath ) # Extract the zip file name without extension $zipName = [System.IO.Path]::GetFileNameWithoutExtension($ZipPath) # Assuming zip file name contains the timestamp in 'yyyyMMdd_HHmmss' format $timestampString = $zipName -replace '^.*?(\d{8}_\d{6}).*$', '$1' try { return [datetime]::ParseExact($timestampString, 'yyyyMMdd_HHmmss', $null) } catch { Write-Log -Message "Failed to parse timestamp from zip file name '$zipName'. Ensure the zip file name contains a timestamp in 'yyyyMMdd_HHmmss' format." -Type "ERROR" return $null } } # Create a list of objects containing zip paths and their timestamps $backupList = @() foreach ($zip in $backupZips) { if (!(Test-Path -Path $zip)) { Write-Log -Message "Backup zip file not found: $zip" -Type "ERROR" continue } $timestamp = Get-ZipTimestamp -ZipPath $zip if ($timestamp -ne $null) { $backupList += [PSCustomObject]@{ ZipPath = $zip Timestamp = $timestamp } } } # Check if we have at least two valid backups if ($backupList.Count -lt 2) { Write-Log -Message "Not enough valid backup versions found for $CompanyName\$FileName." -Type "ERROR" exit 1 } # Sort the backups by timestamp in descending order (latest first) $sortedBackups = $backupList | Sort-Object -Property Timestamp -Descending # Select the top two backups $latestBackup = $sortedBackups[0] $previousBackup = $sortedBackups[1] Write-Log -Message "Latest backup: $($latestBackup.ZipPath) (Timestamp: $($latestBackup.Timestamp))" -Type "INFO" Write-Log -Message "Previous backup: $($previousBackup.ZipPath) (Timestamp: $($previousBackup.Timestamp))" -Type "INFO" # ----------- Step 6: Unzip the Two Folders for Inspection ----------- # Ensure temporary extraction directories exist try { New-Item -ItemType Directory -Path $tempDirLatest -Force | Out-Null New-Item -ItemType Directory -Path $tempDirPrevious -Force | Out-Null Write-Log -Message "Created temporary directories for extraction." -Type "INFO" } catch { Write-Log -Message "Failed to create temporary extraction directories: $_" -Type "ERROR" exit 1 } # Function to extract a specific file from a zip archive function Extract-SpecificFile { param ( [string]$ZipPath, [string]$CompanyName, [string]$FileName, [string]$DestinationDir ) try { # Expand-Archive extracts all files; to optimize, you could extract only the specific file if needed Expand-Archive -Path $ZipPath -DestinationPath $DestinationDir -Force Write-Log -Message "Extracted '$FileName' from '$ZipPath' to '$DestinationDir'." -Type "INFO" } catch { Write-Log -Message "Failed to extract '$FileName' from '$ZipPath': $_" -Type "ERROR" throw $_ } } # Extract the specified file from the latest backup try { Extract-SpecificFile -ZipPath $latestBackup.ZipPath -CompanyName $CompanyName -FileName $FileName -DestinationDir $tempDirLatest } catch { Write-Log -Message "Extraction failed for the latest backup. Aborting comparison." -Type "ERROR" # Cleanup before exiting Remove-Item -Path $tempDirLatest -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempDirPrevious -Recurse -Force -ErrorAction SilentlyContinue exit 1 } # Extract the specified file from the previous backup try { Extract-SpecificFile -ZipPath $previousBackup.ZipPath -CompanyName $CompanyName -FileName $FileName -DestinationDir $tempDirPrevious } catch { Write-Log -Message "Extraction failed for the previous backup. Aborting comparison." -Type "ERROR" # Cleanup before exiting Remove-Item -Path $tempDirLatest -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempDirPrevious -Recurse -Force -ErrorAction SilentlyContinue exit 1 } # ----------- Step 7: Locate the Files in Each Folder ----------- # Construct the expected file paths after extraction $extractedFileLatest = Join-Path -Path (Join-Path -Path $tempDirLatest -ChildPath $CompanyName) -ChildPath $FileName $extractedFilePrevious = Join-Path -Path (Join-Path -Path $tempDirPrevious -ChildPath $CompanyName) -ChildPath $FileName # Verify that the extracted files exist if (!(Test-Path -Path $extractedFileLatest)) { Write-Log -Message "Latest extracted file not found at '$extractedFileLatest'." -Type "ERROR" # Cleanup before exiting Remove-Item -Path $tempDirLatest -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempDirPrevious -Recurse -Force -ErrorAction SilentlyContinue exit 1 } if (!(Test-Path -Path $extractedFilePrevious)) { Write-Log -Message "Previous extracted file not found at '$extractedFilePrevious'." -Type "ERROR" # Cleanup before exiting Remove-Item -Path $tempDirLatest -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempDirPrevious -Recurse -Force -ErrorAction SilentlyContinue exit 1 } # ----------- Step 8: Run a Diff Against the Two Files ----------- # Determine if the files are text or binary based on extension $extension = [System.IO.Path]::GetExtension($FileName).ToLower() # Function to compare text files function Compare-TextFiles { param ( [string]$ReferenceFile, [string]$DifferenceFile ) try { # Read all lines from both files $refLines = Get-Content -Path $ReferenceFile $diffLines = Get-Content -Path $DifferenceFile # Determine the maximum number of lines between both files $maxLines = [Math]::Max($refLines.Count, $diffLines.Count) # Initialize an array to store detailed differences $differences = @() # Loop through each line number for ($i = 0; $i -lt $maxLines; $i++) { $lineNumber = $i + 1 $refLine = if ($i -lt $refLines.Count) { $refLines[$i] } else { $null } $diffLine = if ($i -lt $diffLines.Count) { $diffLines[$i] } else { $null } if ($refLine -ne $diffLine) { if ($refLine -ne $null -and $diffLine -ne $null) { # Line exists in both files but is different (Modified) $differences += "Line ${lineNumber}: Modified" $differences += " Reference: $refLine" $differences += " Difference: $diffLine" } elseif ($refLine -ne $null) { # Line exists only in the reference file (Removed) $differences += "Line ${lineNumber}: Removed" $differences += " Reference: $refLine" } elseif ($diffLine -ne $null) { # Line exists only in the difference file (Added) $differences += "Line ${lineNumber}: Added" $differences += " Difference: $diffLine" } } } if ($differences.Count -gt 0) { Write-Log -Message "Differences found between '$ReferenceFile' and '$DifferenceFile':" -Type "INFO" # Log each difference foreach ($diff in $differences) { Write-Log -Message $diff -Type "INFO" } # Define the differences output file path # Ensure that $logFile and $timestamp are accessible within this function if ($global:LogFile -and $global:timestamp) { $logDir = Split-Path -Path $global:LogFile -Parent $differenceFileName = "$(Split-Path -Path $DifferenceFile -Leaf)-Differences-$($global:timestamp).txt" # Sanitize the difference file name to remove any invalid characters $invalidChars = [System.IO.Path]::GetInvalidFileNameChars() foreach ($char in $invalidChars) { $differenceFileName = $differenceFileName -replace [regex]::Escape($char), '_' } $diffFilePath = Join-Path -Path $logDir -ChildPath $differenceFileName } else { # Fallback if $global:LogFile or $global:timestamp is not defined $logDir = Split-Path -Path $DifferenceFile -Parent $differenceFileName = "Differences-$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" $diffFilePath = Join-Path -Path $logDir -ChildPath $differenceFileName } # Export differences to the separate file $differences | Out-File -FilePath $diffFilePath -Encoding UTF8 Write-Output $differences Write-Log -Message "Differences exported to '$diffFilePath'." -Type "INFO" } else { Write-Output "No differences found between '$ReferenceFile' and '$DifferenceFile'." Write-Log -Message "No differences found between '$ReferenceFile' and '$DifferenceFile'." -Type "INFO" } } catch { Write-Log -Message "Failed to compare text files: $_" -Type "ERROR" } } # Function to compare binary files using hash comparison function Compare-BinaryFiles { param ( [string]$ReferenceFile, [string]$DifferenceFile ) try { $hashReference = Get-FileHash -Path $ReferenceFile -Algorithm SHA256 $hashDifference = Get-FileHash -Path $DifferenceFile -Algorithm SHA256 if ($hashReference.Hash -ne $hashDifference.Hash) { Write-Log -Message "Hash mismatch detected between '$ReferenceFile' and '$DifferenceFile'." -Type "INFO" Write-Log -Message "Reference Hash: $($hashReference.Hash)" -Type "INFO" Write-Log -Message "Difference Hash: $($hashDifference.Hash)" -Type "INFO" } else { Write-Log -Message "No hash differences found between '$ReferenceFile' and '$DifferenceFile'." -Type "INFO" } } catch { Write-Log -Message "Failed to compare binary files: $_" -Type "ERROR" } } # Execute the appropriate comparison based on file type if ($extension -eq '.dat' -or $extension -eq '.nwh') { # Assuming .DAT and .NWH are text files; adjust if they are binary Compare-TextFiles -ReferenceFile $extractedFilePrevious -DifferenceFile $extractedFileLatest } else { # For other file types, perform binary comparison Compare-BinaryFiles -ReferenceFile $extractedFilePrevious -DifferenceFile $extractedFileLatest } # ----------- Step 9: Cleanup Temporary Directories ----------- try { Remove-Item -Path $tempDirLatest -Recurse -Force Remove-Item -Path $tempDirPrevious -Recurse -Force Remove-Item -Path $tempExtractionRoot -Recurse -Force Write-Log -Message "Cleaned up temporary extraction directories." -Type "INFO" } catch { Write-Log -Message "Failed to remove temporary directories: $_" -Type "WARNING" } # ----------- Step 10: Completion Message ----------- Write-Log -Message "Comparison process completed for $CompanyName\$FileName." -Type "INFO" Write-Host "Comparison completed. Check the log file at '$logFile' for details."