Private/Sync-OSDAppCache.ps1

function Sync-OSDAppCache {
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Uri')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Uri')]
        [uri]$CatalogUri,

        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [string]$CatalogPath,

        [Parameter(Mandatory)]
        [string]$CachePath,

        [string[]]$Name
    )

    $logPath = Join-Path $CachePath 'Logs\\Client.log'

    Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'SyncStart' -Message 'Starting repository synchronization.' -Data @{ CachePath = $CachePath; ParameterSet = $PSCmdlet.ParameterSetName }

    if ($PSCmdlet.ParameterSetName -eq 'Uri') {
        $sourceCatalog = Get-OSDAppCatalogPackages -CatalogUri $CatalogUri -Name $Name
        $catalogSourceDescription = $CatalogUri.AbsoluteUri
        $repositoryRoot = $null
        $repositoryBaseUri = [uri]::new($CatalogUri, '.')
    }
    else {
        $resolvedCatalogPath = (Resolve-Path -LiteralPath $CatalogPath -ErrorAction Stop).Path
        $sourceCatalog = Get-OSDAppCatalogPackages -CatalogPath $resolvedCatalogPath -Name $Name
        $catalogSourceDescription = $resolvedCatalogPath
        $repositoryRoot = Split-Path -Path $resolvedCatalogPath -Parent
        $repositoryBaseUri = $null
    }

    $hostArchitecture = Get-OSDAppHostArchitecture
    $allPackages = @($sourceCatalog.Packages)

    # Resolve one compatible package variant per application.
    # Exact architecture always wins; 'any' is the fallback.
    $packages = @(
        foreach ($group in ($allPackages | Group-Object Id)) {
            $exact = @(
                $group.Group |
                    Where-Object { ([string]$_.Architecture).ToLowerInvariant() -eq $hostArchitecture }
            )

            $neutral = @(
                $group.Group |
                    Where-Object { ([string]$_.Architecture).ToLowerInvariant() -eq 'any' }
            )

            if ($exact.Count -gt 0) {
                $exact | Select-Object -First 1
            }
            elseif ($neutral.Count -gt 0) {
                $neutral | Select-Object -First 1
            }
            else {
                throw "No compatible package variant for '$($group.Name)' on '$hostArchitecture'. Available: $((@($group.Group.Architecture) -join ', '))"
            }
        }
    )

    Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'ArchitectureResolved' -Message 'Resolved package variants for host architecture.' -Data @{ HostArchitecture = $hostArchitecture; Packages = @($packages | ForEach-Object { "$($_.Id):$($_.Architecture)" }) }

    $packagesRoot = Join-Path $CachePath 'Packages'
    $stagingRoot  = Join-Path $CachePath '.staging'
    New-Item -ItemType Directory -Path $packagesRoot -Force | Out-Null
    New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null

    foreach ($package in $packages) {
        if (-not $package.Id -or -not $package.Archive -or -not $package.Archive.Sha256) {
            throw "Package '$($package.Id)' has an incomplete Archive definition."
        }

        if ($package.Archive.FileName -and $package.Archive.FileName -ne 'Package.zip') {
            throw "Package '$($package.Id)' must use the fixed archive name Package.zip."
        }

        $targetRoot = Join-Path $packagesRoot $package.Id
        $targetArchive = Join-Path $targetRoot 'Package.zip'

        if (Test-OSDAppFileHash -Path $targetArchive -ExpectedSha256 $package.Archive.Sha256) {
            Write-Verbose "$($package.Id) is current."
            Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'PackageCurrent' -Message 'Cached package is current.' -Data @{ Id = $package.Id; Version = $package.Version; Architecture = $package.Architecture }
            continue
        }

        if (-not $PSCmdlet.ShouldProcess($package.Id, "Synchronize version $($package.Version)")) { continue }

        $packageTemp = Join-Path $stagingRoot $package.Id
        if (Test-Path -LiteralPath $packageTemp) { Remove-Item -LiteralPath $packageTemp -Recurse -Force }
        New-Item -ItemType Directory -Path $packageTemp -Force | Out-Null
        $tempArchive = Join-Path $packageTemp 'Package.zip'

        if ($package.Archive.Uri) {
            Write-Verbose "Downloading $($package.Archive.Uri)"
            Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'AcquireStart' -Message 'Downloading package archive.' -Data @{ Id = $package.Id; Version = $package.Version; Architecture = $package.Architecture; Source = $package.Archive.Uri }
            Invoke-WebRequest -Uri $package.Archive.Uri -OutFile $tempArchive -UseBasicParsing
        }
        elseif ($package.Archive.SourcePath) {
            $sourceArchive = [string]$package.Archive.SourcePath
            if (-not (Test-Path -LiteralPath $sourceArchive -PathType Leaf)) { throw "Source archive not found: $sourceArchive" }
            Write-Verbose "Copying $sourceArchive"
            Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'AcquireStart' -Message 'Copying package archive.' -Data @{ Id = $package.Id; Version = $package.Version; Architecture = $package.Architecture; Source = $sourceArchive }
            Copy-Item -LiteralPath $sourceArchive -Destination $tempArchive -Force
        }
        else {
            throw "Package '$($package.Id)' archive has neither Uri nor SourcePath."
        }

        if (-not (Test-OSDAppFileHash -Path $tempArchive -ExpectedSha256 $package.Archive.Sha256)) {
            Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'HashValidationFailed' -Level 'Error' -Message 'SHA-256 validation failed.' -Data @{ Id = $package.Id; Version = $package.Version; Architecture = $package.Architecture }
            throw "SHA-256 validation failed for '$($package.Id)/Package.zip'."
        }

        Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'PackageUpdated' -Message 'Package synchronized and validated.' -Data @{ Id = $package.Id; Version = $package.Version; Architecture = $package.Architecture; Sha256 = $package.Archive.Sha256 }

        if (Test-Path -LiteralPath $targetRoot) {
            $backupRoot = "$targetRoot.previous"
            if (Test-Path -LiteralPath $backupRoot) { Remove-Item -LiteralPath $backupRoot -Recurse -Force }
            Move-Item -LiteralPath $targetRoot -Destination $backupRoot
            try {
                Move-Item -LiteralPath $packageTemp -Destination $targetRoot
                Remove-Item -LiteralPath $backupRoot -Recurse -Force
            }
            catch {
                if (Test-Path -LiteralPath $targetRoot) { Remove-Item -LiteralPath $targetRoot -Recurse -Force }
                Move-Item -LiteralPath $backupRoot -Destination $targetRoot
                throw
            }
        }
        else { Move-Item -LiteralPath $packageTemp -Destination $targetRoot }
    }

    $cacheCatalogPath = Join-Path $CachePath 'CacheCatalog.json'

    $existingPackages = @()
    if (Test-Path -LiteralPath $cacheCatalogPath -PathType Leaf) {
        try {
            $existingCatalog = Get-OSDAppManifest -Path $cacheCatalogPath
            $existingPackages = @($existingCatalog.Packages)
        }
        catch {
            Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'CacheCatalogReadFailed' -Level 'Warning' -Message 'Existing cache catalog could not be read and will be rebuilt from synchronized packages.' -Data @{ Catalog = $cacheCatalogPath; Error = $_.Exception.Message }
            $existingPackages = @()
        }
    }

    $updatedIds = @($packages.Id)
    $mergedPackages = @(
        @($existingPackages | Where-Object { $_.Id -notin $updatedIds }) +
        @($packages)
    )

    $selectedCatalog = [ordered]@{
        SchemaVersion = $sourceCatalog.SchemaVersion
        GeneratedAt   = $sourceCatalog.GeneratedAt
        SourceCatalog = $catalogSourceDescription
        Packages      = @($mergedPackages)
    }

    $selectedCatalog | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $cacheCatalogPath -Encoding UTF8

    Write-OSDAppLog -LogPath $logPath -Component 'Sync' -Event 'SyncComplete' -Message 'Repository synchronization completed.' -Data @{
        Catalog = $cacheCatalogPath
        SyncedPackageCount = @($packages).Count
        CachedPackageCount = @($mergedPackages).Count
    }

    Get-Item -LiteralPath $cacheCatalogPath
}