From e46472393a4081c527108c129edb8ac4c561496c Mon Sep 17 00:00:00 2001 From: RobertKohut <> Date: Thu, 2 Jan 2025 18:23:16 -0800 Subject: [PATCH] Initial commit --- .gitignore | 1 + Modules/Logging/Logging.psd1 | 7 + Modules/Logging/Logging.psm1 | 39 ++++ Modules/NWA-Compare.ps1 | 423 +++++++++++++++++++++++++++++++++++ Modules/NWABackup.ps1 | 279 +++++++++++++++++++++++ NWA-Sentinel.ps1 | 258 +++++++++++++++++++++ config.json.sample | 5 + 7 files changed, 1012 insertions(+) create mode 100644 .gitignore create mode 100644 Modules/Logging/Logging.psd1 create mode 100644 Modules/Logging/Logging.psm1 create mode 100644 Modules/NWA-Compare.ps1 create mode 100644 Modules/NWABackup.ps1 create mode 100644 NWA-Sentinel.ps1 create mode 100644 config.json.sample diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/Modules/Logging/Logging.psd1 b/Modules/Logging/Logging.psd1 new file mode 100644 index 0000000..d323198 --- /dev/null +++ b/Modules/Logging/Logging.psd1 @@ -0,0 +1,7 @@ +@{ + ModuleVersion = '1.0.0' + GUID = 'affc6c50-b639-478a-a2f2-b7356f1ace16' + Author = 'Alex Kohut' + Description = 'Handles logging operations.' + FunctionsToExport = @('Initialize-Logging', 'Write-Log') +} \ No newline at end of file diff --git a/Modules/Logging/Logging.psm1 b/Modules/Logging/Logging.psm1 new file mode 100644 index 0000000..09b5a44 --- /dev/null +++ b/Modules/Logging/Logging.psm1 @@ -0,0 +1,39 @@ +# Modules/Logging/Logging.psm1 + +# Function to initialize the log file path +function Initialize-Logging { + param ( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + # Store the log file path in a module-scoped variable + $Script:LogFile = $Path +} + +# Function to log messages with timestamps +function Write-Log { + param ( + [string]$Message, + [ValidateSet("INFO", "WARNING", "ERROR")] + [string]$Type = "INFO" + ) + + if (-not $Script:LogFile) { + Throw "Log file path is not initialized. Please run Initialize-Logging first." + } + + $logTimestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $logMessage = "$logTimestamp [$Type] - $Message" + + Add-Content -Path $Script:LogFile -Value $logMessage + + switch ($Type) { + "INFO" { Write-Verbose $logMessage } + "WARNING" { Write-Warning $logMessage } + "ERROR" { Write-Error $logMessage } + } +} + +# Export the functions +Export-ModuleMember -Function Initialize-Logging, Write-Log \ No newline at end of file diff --git a/Modules/NWA-Compare.ps1 b/Modules/NWA-Compare.ps1 new file mode 100644 index 0000000..73e1c83 --- /dev/null +++ b/Modules/NWA-Compare.ps1 @@ -0,0 +1,423 @@ +<# +.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." \ No newline at end of file diff --git a/Modules/NWABackup.ps1 b/Modules/NWABackup.ps1 new file mode 100644 index 0000000..720a907 --- /dev/null +++ b/Modules/NWABackup.ps1 @@ -0,0 +1,279 @@ +# -------------------------------------------- +# Northwest Analytics (NWA) Backup Script +# -------------------------------------------- +# This script backs up .DAT and .NWH files from the shared +# directory to a user-specific backup folder, preserving +# the company directory structure and compressing the +# backup into a zip file. It also maintains a global +# manifest in JSON format. +# -------------------------------------------- + +function ConvertTo-Hashtable { + param ( + [Parameter(Mandatory = $true)] + [object]$InputObject + ) + + if ($InputObject -is [System.Management.Automation.PSCustomObject]) { + $hash = @{} + foreach ($prop in $InputObject.PSObject.Properties) { + $hash[$prop.Name] = ConvertTo-Hashtable -InputObject $prop.Value + } + return $hash + } + elseif ($InputObject -is [System.Array]) { + return $InputObject | ForEach-Object { ConvertTo-Hashtable -InputObject $_ } + } + else { + return $InputObject + } +} + +# ----------- 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 + +# ----------- Step 1: Define Variables ----------- + +$sharedDirectory = "X:\dev\BOYD\%NWA\Data Collection" # Using environment variable +$username = $env:USERNAME +$backupRoot = "C:\Users\$username\Desktop\NWABackup" +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$daysBack = 4 +$thresholdDate = (Get-Date).AddDays(-$daysBack) + +$backupDirectory = Join-Path -Path $backupRoot -ChildPath $timestamp +$zipFilePath = Join-Path -Path $backupRoot -ChildPath "$timestamp.zip" +$manifestFilePath = Join-Path -Path $backupRoot -ChildPath "manifest.json" + + +# Define log file path +$logDirectory = Join-Path -Path $backupRoot -ChildPath "Logs" +$logFile = Join-Path -Path $logDirectory -ChildPath "$timestamp.log" + +# ----------- 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 backup process +Write-Log -Message "=== NWABackup Process Started at $timestamp ===" -Type "INFO" + +# ----------- Step 3: Create Backup Directory ----------- + +if (!(Test-Path -Path $backupDirectory)) { + try { + New-Item -ItemType Directory -Path $backupDirectory -Force | Out-Null + Write-Log -Message "Backup directory created at ${backupDirectory}" -Type "INFO" + } + catch { + Write-Log -Message "Failed to create backup directory at ${backupDirectory}. Error: $_" -Type "ERROR" + exit 1 + } +} +else { + Write-Log -Message "Backup directory already exists at ${backupDirectory}" -Type "INFO" +} + +# ----------- Step 4: Copy .DAT and .NWH Files ----------- + +# Define the file extensions to include (case-insensitive) +$fileExtensions = @('.DAT', '.NWH') + +Write-Log -Message "Starting file retrieval and copy process." -Type "INFO" + +# Get all files recursively and filter by extension and modification date in a single pass +try { + $files = Get-ChildItem -Path $sharedDirectory -Recurse -File | Where-Object { + ($fileExtensions -contains $_.Extension.ToUpper()) -and ($_.LastWriteTime -ge $thresholdDate) + } + Write-Log -Message "Found $($files.Count) files with specified extensions modified in the last $daysBack days." -Type "INFO" +} +catch { + Write-Log -Message "Error retrieving files: $_" -Type "ERROR" + exit 1 +} + +# Initialize a hashtable to build the manifest +$manifest = @{} + +foreach ($file in $files) { + # Extract company name from the folder structure + $relativePath = $file.FullName.Substring($sharedDirectory.Length).TrimStart("\") + $pathParts = $relativePath.Split("\") + if ($pathParts.Length -lt 2) { + Write-Log -Message "Invalid file path structure: $relativePath" -Type "WARNING" + continue + } + + $companyName = $pathParts[0] + $fileName = $pathParts[-1] + + # Extract part name and file type from the file name + $partName, $fileType = $fileName -split '\.', 2 + + if (-not $fileType) { + Write-Log -Message "Invalid file format: $fileName" -Type "WARNING" + continue + } + + # Initialize company entry if not exists + if (-not $manifest.ContainsKey($companyName)) { + $manifest[$companyName] = @{} + } + + # Initialize part entry if not exists + if (-not $manifest[$companyName].ContainsKey($partName)) { + $manifest[$companyName][$partName] = @{} # This must be a hash table, not an array + } + + # Initialize file type entry if not exists + if (-not $manifest[$companyName][$partName].ContainsKey($fileType)) { + $manifest[$companyName][$partName][$fileType] = @() + } + + # Add the current backup zip file to the file type's backup array + if (-not $manifest[$companyName][$partName][$fileType].Contains($zipFilePath)) { + $manifest[$companyName][$partName][$fileType] += $zipFilePath + } + + # Define the destination path + $destinationPath = Join-Path -Path $backupDirectory -ChildPath $relativePath + + # Get the destination directory + $destinationDir = Split-Path -Path $destinationPath -Parent + + # Create the destination directory if it doesn't exist + if (!(Test-Path -Path $destinationDir)) { + try { + New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null + Write-Log -Message "Created directory: ${destinationDir}" -Type "INFO" + } + catch { + Write-Log -Message "Failed to create directory ${destinationDir}: $_" -Type "ERROR" + continue + } + } + + # Copy the file to the backup directory + try { + Copy-Item -Path $file.FullName -Destination $destinationPath -Force -ErrorAction Stop + Write-Log -Message "Copied: $($file.FullName) to ${destinationPath}" -Type "INFO" + } + catch { + Write-Log -Message "Failed to copy $($file.FullName) to ${destinationPath}: $_" -Type "ERROR" + } +} + + + +# ----------- Step 5: Update and Save the Manifest JSON ----------- +if (Test-Path -Path $manifestFilePath) { + try { + $existingManifest = Get-Content -Path $manifestFilePath | ConvertFrom-Json + Write-Log -Message "Loaded existing manifest file." -Type "INFO" + } + catch { + Write-Log -Message "Failed to load existing manifest file. Creating a new one. Error: $_" -Type "ERROR" + $existingManifest = @{} + } +} +else { + $existingManifest = @{} +} + +# Convert PSCustomObject to Hashtable +$existingManifest = ConvertTo-Hashtable -InputObject $existingManifest + +# Merge the current manifest into the existing manifest +foreach ($company in $manifest.Keys) { + if (-not $existingManifest.ContainsKey($company)) { + $existingManifest[$company] = @{} + Write-Log -Message "Added new company '$company' to manifest." -Type "INFO" + } + + foreach ($partName in $manifest[$company].Keys) { + if (-not $existingManifest[$company].ContainsKey($partName)) { + $existingManifest[$company][$partName] = @{} + Write-Log -Message "Added new part '$partName' under company '$company' to manifest." -Type "INFO" + } + + foreach ($fileType in $manifest[$company][$partName].Keys) { + if (-not $existingManifest[$company][$partName].ContainsKey($fileType)) { + $existingManifest[$company][$partName][$fileType] = @() + Write-Log -Message "Added new file type '$fileType' under part '$partName' for company '$company' to manifest." -Type "INFO" + } + + foreach ($zip in $manifest[$company][$partName][$fileType]) { + # Ensure the value is treated as an array + if ($existingManifest[$company][$partName][$fileType] -isnot [System.Collections.ArrayList]) { + $existingManifest[$company][$partName][$fileType] = @($existingManifest[$company][$partName][$fileType]) + } + + # Add the zip path to the array if it doesn't already exist + if (-not $existingManifest[$company][$partName][$fileType].Contains($zip)) { + $existingManifest[$company][$partName][$fileType] += $zip + Write-Log -Message "Added zip '$zip' to '$company\\$partName\\$fileType' in manifest." -Type "INFO" + } + } + } + } +} + +# Convert the hashtable to JSON with formatting +$jsonContent = $existingManifest | ConvertTo-Json -Depth 10 -Compress:$false + +# Save the updated manifest +try { + Set-Content -Path $manifestFilePath -Value $jsonContent -Encoding UTF8 + Write-Log -Message "Manifest file updated at $manifestFilePath" -Type "INFO" +} +catch { + Write-Log -Message "Failed to update manifest file at $manifestFilePath. Error: $_" -Type "ERROR" +} + + +# ----------- Step 6: Compress the Backup Directory ----------- + +try { + # Compress the *contents* of the backup directory, not the directory itself + Compress-Archive -Path "$backupDirectory\*" -DestinationPath $zipFilePath -Force + Write-Log -Message "Successfully compressed backup to $zipFilePath" -Type "INFO" +} +catch { + Write-Log -Message "Failed to compress backup directory: $_" -Type "ERROR" + # Optionally, decide whether to exit or continue +} + +# ----------- Step 7: Cleanup (Optional) ----------- + +# Optionally, remove the uncompressed backup directory after compression +try { + Remove-Item -Path $backupDirectory -Recurse -Force + Write-Log -Message "Removed uncompressed backup directory: ${backupDirectory}" -Type "INFO" +} +catch { + Write-Log -Message "Failed to remove uncompressed backup directory ${backupDirectory}: $_" -Type "ERROR" +} + +# ----------- Step 8: Completion Message ----------- + +Write-Log -Message "Backup process completed." -Type "INFO" + +# Optionally, display a message to the user +Write-Host "Backup completed successfully. Log file located at $logFile" diff --git a/NWA-Sentinel.ps1 b/NWA-Sentinel.ps1 new file mode 100644 index 0000000..0160ad7 --- /dev/null +++ b/NWA-Sentinel.ps1 @@ -0,0 +1,258 @@ +$configFilePath = Join-Path -Path $PSScriptRoot -ChildPath "config.json" + +# Check if the configuration file exists +if (-Not (Test-Path $configFilePath)) { + Write-Error "Configuration file not found at $configFilePath" + exit 1 +} + +# Load the configuration file +$config = Get-Content $configFilePath | ConvertFrom-Json + +# Variables for script paths +$backupScriptPath = $config.BackupScriptPath +$compareScriptPath = $config.CompareScriptPath +$outputPath = $config.OutputPath + +# Import necessary assemblies for GUI +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +$processing = $false + +# Create Form +$form = New-Object System.Windows.Forms.Form +$form.Text = "NWA Sentinel" +$form.Size = New-Object System.Drawing.Size(800, 700) +$form.StartPosition = "CenterScreen" + +# Create Backup Button +$backupButton = New-Object System.Windows.Forms.Button +$backupButton.Text = "Backup" +$backupButton.Size = New-Object System.Drawing.Size(100, 30) +$backupButton.Location = New-Object System.Drawing.Point(10, 10) +$form.Controls.Add($backupButton) + +# Create Company List Box +$companyList = New-Object System.Windows.Forms.ListBox +$companyList.Size = New-Object System.Drawing.Size(200, 300) +$companyList.Location = New-Object System.Drawing.Point(10, 50) +$form.Controls.Add($companyList) + +# Create File Details List Box +$fileDetailsListBox = New-Object System.Windows.Forms.ListBox +$fileDetailsListBox.Size = New-Object System.Drawing.Size(550, 300) +$fileDetailsListBox.Location = New-Object System.Drawing.Point(220, 50) +$form.Controls.Add($fileDetailsListBox) + +# Create Compare Button +$compareButton = New-Object System.Windows.Forms.Button +$compareButton.Text = "Compare" +$compareButton.Size = New-Object System.Drawing.Size(100, 30) +$compareButton.Location = New-Object System.Drawing.Point(10, 360) +$compareButton.Enabled = $false +$form.Controls.Add($compareButton) + +# Create QXP Button +$qxpButton = New-Object System.Windows.Forms.Button +$qxpButton.Text = "QXP" +$qxpButton.Size = New-Object System.Drawing.Size(100, 30) +$qxpButton.Location = New-Object System.Drawing.Point(120, 360) +$qxpButton.Enabled = $false +$form.Controls.Add($qxpButton) + +# Create Compare Output Text Box +$compareOutputBox = New-Object System.Windows.Forms.TextBox +$compareOutputBox.Multiline = $true +$compareOutputBox.ReadOnly = $true +$compareOutputBox.ScrollBars = "Vertical" +$compareOutputBox.Size = New-Object System.Drawing.Size(760, 200) +$compareOutputBox.Location = New-Object System.Drawing.Point(10, 400) +$form.Controls.Add($compareOutputBox) + +# Load Company Names into ListBox from manifest.json +$manifestPath = "C:\Users\$env:USERNAME\Desktop\NWABackup\manifest.json" +$manifestData = $null +if (Test-Path $manifestPath) { + try { + $manifestData = Get-Content -Path $manifestPath | ConvertFrom-Json + $companyNames = $manifestData.PSObject.Properties.Name + $companyList.Items.AddRange($companyNames) + } catch { + [System.Windows.Forms.MessageBox]::Show("Failed to parse manifest.json: $_", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + } +} else { + [System.Windows.Forms.MessageBox]::Show("Manifest file not found at $manifestPath", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) +} + +# Event: When a company is selected, populate file details +$companyList.Add_SelectedIndexChanged({ + $selectedCompany = $companyList.SelectedItem + $compareOutputBox.Text = "" + $qxpButton.Enabled = $false # Disable the QXP button when the company changes + + if ($selectedCompany -and $manifestData) { + $fileDetailsListBox.Items.Clear() + try { + # Retrieve the file groups for the selected company + $fileGroups = $manifestData.$selectedCompany.PSObject.Properties + + foreach ($group in $fileGroups) { + $fileData = $group.Value + $datFiles = $fileData.DAT + $qxpFiles = $fileData.QXP + + # Extract dates from zip file paths + $dates = $datFiles | ForEach-Object { + if ($_ -match "\\(\d{8})_(\d{6})\.zip$") { + # Combine date and time into a single string + $dateTimeString = "$($matches[1])$($matches[2])" + [datetime]::ParseExact($dateTimeString, "yyyyMMddHHmmss", $null) + } else { + $null + } + } + + # Get the latest date + $latestDate = $dates | Where-Object { $_ } | Sort-Object -Descending | Select-Object -First 1 + + # Display the file group and its latest date + $latestDateText = if ($latestDate) { + $latestDate.ToString("yyyy-MM-dd HH:mm:ss") + } else { + "No valid date found" + } + + $groupName = [string]$group.Name + $fileDetailsListBox.Items.Add("$latestDateText :: $groupName") + } + } catch { + [System.Windows.Forms.MessageBox]::Show("Error processing files for company '$selectedCompany': $_", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + } + } +}) + +$fileDetailsListBox.Add_SelectedIndexChanged({ + $compareOutputBox.Text = "" # Clear the output box + $compareButton.Enabled = $true # Enable the Compare button + + $selectedFileGroup = $fileDetailsListBox.SelectedItem + $selectedCompany = $companyList.SelectedItem + $qxpButton.Enabled = $false # Disable the QXP button by default + + if ($selectedFileGroup -and $selectedCompany -and $manifestData) { + try { + # Extract the part name from the selected file group + $partName = $selectedFileGroup -replace ".* :: ", "" + + # Check if the selected part has QXP files + $qxpFiles = $manifestData.$selectedCompany.$partName.QXP + + if ($qxpFiles -and $qxpFiles.Count -gt 0) { + $qxpButton.Enabled = $true + } + } catch { + [System.Windows.Forms.MessageBox]::Show("Error checking QXP availability for '$selectedFileGroup': $_", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + } + } +}) + +# Backup Button Click Event +$backupButton.Add_Click({ + if (Test-Path $backupScriptPath) { + Start-Process -FilePath "powershell.exe" -ArgumentList "-File `"$backupScriptPath`"" -NoNewWindow -Wait + [System.Windows.Forms.MessageBox]::Show("Backup completed.", "Information", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information) + } else { + [System.Windows.Forms.MessageBox]::Show("Backup script not found at $backupScriptPath", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + } +}) + +# Compare Button Click Event +$compareButton.Add_Click({ + if ($processing) { + [System.Windows.Forms.MessageBox]::Show("Please wait for the current operation to complete.", "Info", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information) + return + } + + $selectedCompany = $companyList.SelectedItem + $selectedFileGroup = $fileDetailsListBox.SelectedItem + + if (-not $selectedCompany) { + [System.Windows.Forms.MessageBox]::Show("Please select a company from the list.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + return + } + + if (-not $selectedFileGroup) { + [System.Windows.Forms.MessageBox]::Show("Please select a file group from the list.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + return + } + + try { + $processing = $true + $backupButton.Enabled = $false + $compareButton.Enabled = $false + + $compareOutputBox.text = "" + # Extract the file name (ignore the date and time) + $fileName = $selectedFileGroup -replace ".* :: ", "" + + # Construct the PowerShell command + $command = "$compareScriptPath -CompanyName '$selectedCompany' -FileName '$fileName.DAT'" + + # Run the compare script and redirect output + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = "powershell.exe" + $processInfo.Arguments = "-NoProfile -Command `"$command`"" + $processInfo.RedirectStandardOutput = $true + $processInfo.RedirectStandardError = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo + $process.Start() | Out-Null + + # Read both standard output and error output + $output = $process.StandardOutput.ReadToEnd() + $errorOutput = $process.StandardError.ReadToEnd() + $process.WaitForExit() + + # Display the output or errors in the Compare Output TextBox + if ($output) { + $compareOutputBox.Text = $output -join "`r`n" + } elseif ($errorOutput) { + $compareOutputBox.Text = "Error: $errorOutput" + } else { + $compareOutputBox.Text = "No output received from the compare script." + } + + } catch { + [System.Windows.Forms.MessageBox]::Show("Error running compare script: $($_.Exception.Message)", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + } finally { + $processing = $false + $backupButton.Enabled = $true + } +}) + +# Event: QXP Button Click +$qxpButton.Add_Click({ + $selectedCompany = $companyList.SelectedItem + if (-not $selectedCompany) { + [System.Windows.Forms.MessageBox]::Show("Please select a company from the list.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + return + } + + $selectedFileGroup = $fileDetailsListBox.SelectedItem + if (-not $selectedFileGroup) { + [System.Windows.Forms.MessageBox]::Show("Please select a file group from the list.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + return + } + + $fileName = $selectedFileGroup -replace ".* - ", "" + [System.Windows.Forms.MessageBox]::Show("Opening QXP file for ${selectedCompany}: $fileName", "QXP File", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information) + + # Implement the actual logic for opening the QXP file if needed +}) + +# Show Form +$form.ShowDialog() \ No newline at end of file diff --git a/config.json.sample b/config.json.sample new file mode 100644 index 0000000..419b78f --- /dev/null +++ b/config.json.sample @@ -0,0 +1,5 @@ +{ + "BackupScriptPath": "X:\\dev\\BOYD\\sentinel\\Modules\\NWABackup.ps1", + "CompareScriptPath": "X:\\dev\\BOYD\\sentinel\\Modules\\NWA-Compare.ps1", + "OutputPath": "C:\\Users\\$env:USERNAME\\Desktop\\NWABackup\\logs\\compare-output.txt" +} \ No newline at end of file