Private/Get-InforcerSettingsCatalogPath.ps1
|
function Get-InforcerSettingsCatalogPath { <# .SYNOPSIS Resolves the path to settings.json via a 6-tier cache strategy. .DESCRIPTION Resolution order: 1. Explicit path (if provided and file exists) 2. Fresh local cache (< 24h old) 3. Stale local cache (> 24h) with remote freshness check 4. First-time download from GitHub Release 5. Fallback: no cache, download failed -> $null (caller proceeds without catalog) 6. Offline with stale cache -> return stale path with warning .PARAMETER ExplicitPath User-specified path to settings.json. If provided and valid, returned immediately. .PARAMETER CacheDirectory Override cache directory. Defaults to ~/.inforcercommunity/data. .PARAMETER BaseUrl Override base URL for GitHub Release assets. For testing. #> [CmdletBinding()] param( [Parameter()] [string]$ExplicitPath, [Parameter()] [string]$CacheDirectory, [Parameter()] [string]$BaseUrl = 'https://github.com/royklo/IntuneSettingsCatalogData/releases/latest/download' ) # Tier 1: Explicit path if (-not [string]::IsNullOrEmpty($ExplicitPath)) { if (Test-Path -LiteralPath $ExplicitPath) { Write-Verbose "Using explicit settings catalog path: $ExplicitPath" return $ExplicitPath } Write-Warning "Explicit settings catalog path not found: $ExplicitPath" return $null } # Resolve cache directory if ([string]::IsNullOrEmpty($CacheDirectory)) { $CacheDirectory = Join-Path ([System.Environment]::GetFolderPath('UserProfile')) '.inforcercommunity' 'data' } $settingsPath = Join-Path $CacheDirectory 'settings.json' $categoriesPath = Join-Path $CacheDirectory 'categories.json' $metaPath = Join-Path $CacheDirectory 'cache-meta.json' $hasCachedFile = Test-Path -LiteralPath $settingsPath # Read cache metadata $cacheMeta = $null if (Test-Path -LiteralPath $metaPath) { try { $cacheMeta = Get-Content -Path $metaPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Write-Verbose "Could not read cache-meta.json: $_" } } # Tier 2: Fresh cache (< 24h) if ($hasCachedFile -and $null -ne $cacheMeta -and $cacheMeta.lastChecked) { # ConvertFrom-Json may auto-deserialize ISO strings to DateTime objects if ($cacheMeta.lastChecked -is [datetime]) { $lastChecked = $cacheMeta.lastChecked.ToUniversalTime() } else { $lastChecked = [datetime]::Parse($cacheMeta.lastChecked, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind) } $age = [datetime]::UtcNow - $lastChecked if ($age.TotalHours -lt 24) { Write-Verbose "Using fresh cached settings catalog (age: $([math]::Round($age.TotalHours, 1))h)" return $settingsPath } } # Tier 3/4: Need to check remote or download $lastUpdatedUrl = "$BaseUrl/last-updated.json" $settingsUrl = "$BaseUrl/settings.json" $categoriesUrl = "$BaseUrl/categories.json" # Save and restore ProgressPreference (disable progress bar for perf) $originalProgress = $ProgressPreference try { $ProgressPreference = 'SilentlyContinue' # Check remote freshness $remoteInfo = $null try { $response = Invoke-WebRequest -Uri $lastUpdatedUrl -UseBasicParsing -ErrorAction Stop $remoteInfo = $response.Content | ConvertFrom-Json } catch { Write-Verbose "Could not fetch last-updated.json: $_" } # Schema version check if ($null -ne $remoteInfo -and $remoteInfo.schemaVersion -and $remoteInfo.schemaVersion -ne 1) { Write-Warning "Settings catalog data has schema version $($remoteInfo.schemaVersion) but this module supports version 1. Update the InforcerCommunity module for full compatibility." if ($hasCachedFile) { Write-Warning 'Falling back to cached settings catalog.' return $settingsPath } return $null } # Tier 3: Stale cache -- check if remote is newer if ($hasCachedFile -and $null -ne $cacheMeta -and $null -ne $remoteInfo) { if ($cacheMeta.releaseTimestamp -eq $remoteInfo.updatedAt) { # Remote unchanged -- refresh lastChecked and use cache $cacheMeta.lastChecked = [datetime]::UtcNow.ToString('o') $cacheMeta | ConvertTo-Json | Set-Content -Path $metaPath -Encoding UTF8 Write-Verbose 'Remote data unchanged -- refreshed cache TTL' return $settingsPath } Write-Verbose 'Remote data is newer than cache -- downloading update' } # Tier 4: Download (first time or update needed) if ($null -ne $remoteInfo -or -not $hasCachedFile) { if (-not (Test-Path -LiteralPath $CacheDirectory)) { New-Item -ItemType Directory -Path $CacheDirectory -Force | Out-Null } $downloaded = $false $maxAttempts = 2 # Single retry for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { try { Write-Host 'Downloading Settings Catalog data...' -ForegroundColor Cyan $tmpSettings = "$settingsPath.tmp" Invoke-WebRequest -Uri $settingsUrl -OutFile $tmpSettings -UseBasicParsing -ErrorAction Stop Move-Item -Path $tmpSettings -Destination $settingsPath -Force # Also download categories $tmpCategories = "$categoriesPath.tmp" Invoke-WebRequest -Uri $categoriesUrl -OutFile $tmpCategories -UseBasicParsing -ErrorAction Stop Move-Item -Path $tmpCategories -Destination $categoriesPath -Force $downloaded = $true break } catch { # Clean up partial downloads Remove-Item -Path "$settingsPath.tmp" -Force -ErrorAction SilentlyContinue Remove-Item -Path "$categoriesPath.tmp" -Force -ErrorAction SilentlyContinue if ($attempt -lt $maxAttempts) { Write-Verbose "Download attempt $attempt failed: $_. Retrying in 2s..." Start-Sleep -Seconds 2 } else { Write-Verbose "Download failed after $maxAttempts attempts: $_" } } } if ($downloaded) { # Update cache metadata $newMeta = @{ lastChecked = [datetime]::UtcNow.ToString('o') releaseTimestamp = $(if ($remoteInfo) { $remoteInfo.updatedAt } else { [datetime]::UtcNow.ToString('o') }) schemaVersion = $(if ($remoteInfo) { $remoteInfo.schemaVersion } else { 1 }) } $newMeta | ConvertTo-Json | Set-Content -Path $metaPath -Encoding UTF8 Write-Host ' Settings Catalog data cached successfully.' -ForegroundColor Green return $settingsPath } } } finally { $ProgressPreference = $originalProgress } # Tier 5/6: Download failed if ($hasCachedFile) { Write-Warning 'Could not refresh Settings Catalog data. Using stale cached version.' return $settingsPath } Write-Warning 'Settings Catalog data not available. Settings Catalog policies will show raw settingDefinitionId values.' return $null } |