Functions/GenXdev.Data.KeyValueStore/Sync-KeyValueStore.ps1

###############################################################################
<#
.SYNOPSIS
Synchronizes local and OneDrive key-value store JSON files.
 
.DESCRIPTION
Performs two-way synchronization between local and OneDrive shadow JSON files using
a last-modified timestamp strategy. Records are merged based on their last
modification time, with newer versions taking precedence.
 
.PARAMETER SynchronizationKey
Identifies the synchronization scope for the operation. Using "Local" will skip
synchronization as it indicates local-only records.
 
.PARAMETER DatabasePath
Database path for key-value store data files.
 
.EXAMPLE
Sync-KeyValueStore
 
.EXAMPLE
Sync-KeyValueStore -SynchronizationKey "UserSettings"
#>

function Sync-KeyValueStore {

    [CmdletBinding()]
    param(
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            Position = 0,
            HelpMessage = 'Key to identify synchronization scope'
        )]
        [string] $SynchronizationKey = 'Local',
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Database path for key-value store data files'
        )]
        [string] $DatabasePath
        ###############################################################################
    )

    begin {

        # check if custom base path was provided or use default location
        if ([string]::IsNullOrWhiteSpace($DatabasePath)) {

            # construct default base path in local app data folder
            $basePath = "$($ENV:LOCALAPPDATA)\GenXdev.PowerShell\KeyValueStore"
        }
        else {

            # use the provided base path
            $basePath = $DatabasePath
        }

        # construct path to onedrive shadow directory for synchronization
        $shadowPath = GenXdev.FileSystem\Expand-Path `
            "~\OneDrive\GenXdev.PowerShell.SyncObjects\KeyValueStore"

        # log the beginning of sync operation for troubleshooting
        Microsoft.PowerShell.Utility\Write-Verbose `
            "Starting key-value store sync with key: $SynchronizationKey"
    }

    process {

        # skip synchronization for local-only records to avoid unnecessary work
        if ($SynchronizationKey -eq 'Local') {

            # inform user that local-only sync is being skipped
            Microsoft.PowerShell.Utility\Write-Verbose `
                'Skipping sync for local-only key'
            return
        }

        # log store directory paths for debugging and verification purposes
        Microsoft.PowerShell.Utility\Write-Verbose "Local path: $basePath"
        Microsoft.PowerShell.Utility\Write-Verbose "Shadow path: $shadowPath"

        # verify both directories exist before attempting synchronization
        if (-not ([System.IO.Directory]::Exists($basePath) -and
                [System.IO.Directory]::Exists($shadowPath))) {

            # inform user that missing directories are being initialized
            Microsoft.PowerShell.Utility\Write-Verbose `
                'Initializing missing store directories'

            # copy compatible parameters for the initialization function call
            $params = GenXdev.Helpers\Copy-IdenticalParamValues `
                -BoundParameters $PSBoundParameters `
                -FunctionName 'GenXdev.Data\Initialize-KeyValueStores' `
                -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                    -Scope Local `
                    -ErrorAction SilentlyContinue)

            # initialize the key-value store directories if they don't exist
            GenXdev.Data\Initialize-KeyValueStores @params
        }

        # get all JSON files from both directories matching the sync key pattern
        $safeSyncKey = $SynchronizationKey -replace '[\\/:*?"<>|]', '_'
        $filePattern = "${safeSyncKey}_*.json"

        Microsoft.PowerShell.Utility\Write-Verbose `
            "Syncing files matching pattern: $filePattern"

        # collect all matching store files from both locations
        $localFiles = @{}
        $shadowFiles = @{}

        foreach ($file in (Microsoft.PowerShell.Management\Get-ChildItem `
                    -LiteralPath $basePath `
                    -Filter $filePattern `
                    -File `
                    -ErrorAction SilentlyContinue)) {
            $localFiles[$file.Name] = $file.FullName
        }

        foreach ($file in (Microsoft.PowerShell.Management\Get-ChildItem `
                    -LiteralPath $shadowPath `
                    -Filter $filePattern `
                    -File `
                    -ErrorAction SilentlyContinue)) {
            $shadowFiles[$file.Name] = $file.FullName
        }

        # get union of all filenames
        $allFilenames = $localFiles.Keys + $shadowFiles.Keys | `
            Microsoft.PowerShell.Utility\Select-Object -Unique

        # sync each store file
        foreach ($filename in $allFilenames) {
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Syncing store file: $filename"

            $localFilePath = [System.IO.Path]::Combine($basePath, $filename)
            $shadowFilePath = [System.IO.Path]::Combine($shadowPath, $filename)

            # read both store versions
            $localData = GenXdev.FileSystem\ReadJsonWithRetry -FilePath $localFilePath
            $shadowData = GenXdev.FileSystem\ReadJsonWithRetry -FilePath $shadowFilePath

            # merge stores based on last modified timestamps
            $mergedData = @{}

            # add all local keys
            foreach ($key in $localData.Keys) {
                $mergedData[$key] = $localData[$key]
            }

            # merge shadow keys, keeping newer versions
            foreach ($key in $shadowData.Keys) {
                $shadowEntry = $shadowData[$key]

                if ($mergedData.ContainsKey($key)) {
                    $localEntry = $mergedData[$key]

                    # compare timestamps if both have metadata
                    if ($localEntry -is [hashtable] -and
                        $shadowEntry -is [hashtable] -and
                        $localEntry.ContainsKey('lastModified') -and
                        $shadowEntry.ContainsKey('lastModified')) {

                        $localTime = [DateTime]::Parse($localEntry['lastModified'])
                        $shadowTime = [DateTime]::Parse($shadowEntry['lastModified'])

                        # keep newer version
                        if ($shadowTime -gt $localTime) {
                            $mergedData[$key] = $shadowEntry
                        }
                    }
                    else {
                        # no metadata, keep shadow version
                        $mergedData[$key] = $shadowEntry
                    }
                }
                else {
                    # key only exists in shadow, add it
                    $mergedData[$key] = $shadowEntry
                }
            }

            # write merged data to both locations
            GenXdev.FileSystem\WriteJsonAtomic -FilePath $localFilePath -Data $mergedData
            GenXdev.FileSystem\WriteJsonAtomic -FilePath $shadowFilePath -Data $mergedData
        }
    }

    end {

        # log completion of sync operation for audit and troubleshooting
        Microsoft.PowerShell.Utility\Write-Verbose 'Sync operation completed'
    }
}