Public/Sync-FsKnowledgeBase.ps1
Function Sync-FsKnowledgeBase { <# .SYNOPSIS Synchronizes local knowledge base files to FreshService .DESCRIPTION The Sync-FsKnowledgeBase function compares local HTML and JSON files with FreshService and updates FreshService with any changes found in the local repository. This is designed to work with the folder structure created by Export-FsKnowledgeBase. .EXAMPLE Sync-FsKnowledgeBase -SourcePath "C:\MyRepo\Articles" Syncs all changes from the local repository to FreshService .EXAMPLE Sync-FsKnowledgeBase -SourcePath "C:\MyRepo\Articles" -WhatIf Shows what would be synced without making any changes .PARAMETER SourcePath The local folder path containing the knowledge base files .PARAMETER WhatIf Shows what changes would be made without actually making them .PARAMETER Force Forces update of all articles even if timestamps suggest no changes .INPUTS None .OUTPUTS [PSCustomObject] - Returns sync summary with counts of items updated .NOTES Requires FreshService API authentication Expects folder structure: Category/Folder/Article.html + Article.json + metadata files .LINK https://api.freshservice.com/v2/#solution_articles #> [CmdletBinding(SupportsShouldProcess)] Param( [Parameter(Mandatory=$true, Position=0)] [string]$SourcePath, [Parameter(Mandatory=$false)] [switch]$Force ) Begin { Write-Verbose -Message "Starting $($MyInvocation.InvocationName)..." Write-Verbose -Message "Parameters are $($PSBoundParameters | Select-Object -Property *)" Connect-FreshServiceAPI # Initialize counters $syncSummary = @{ CategoriesProcessed = 0 FoldersProcessed = 0 ArticlesUpdated = 0 ArticlesCreated = 0 ArticlesSkipped = 0 Errors = @() StartTime = Get-Date } } Process { try { if (-not (Test-Path $SourcePath)) { throw "Source path does not exist: $SourcePath" } # Get all category folders (exclude .freshservice folder) $categoryFolders = Get-ChildItem -Path $SourcePath -Directory | Where-Object { $_.Name -ne '.freshservice' } foreach ($categoryFolder in $categoryFolders) { Write-Host "Processing category folder: $($categoryFolder.Name)" -ForegroundColor Green $syncSummary.CategoriesProcessed++ # Read category metadata $categoryMetadataPath = Join-Path $categoryFolder.FullName ".category.json" if (Test-Path $categoryMetadataPath) { $categoryMetadata = Get-Content $categoryMetadataPath | ConvertFrom-Json Write-Verbose "Loaded category metadata: ID $($categoryMetadata.id), Name: $($categoryMetadata.name)" } else { Write-Warning "Category metadata file not found: $categoryMetadataPath" continue } # Get all folder directories in this category $folderDirectories = Get-ChildItem -Path $categoryFolder.FullName -Directory foreach ($folderDir in $folderDirectories) { Write-Host " Processing folder: $($folderDir.Name)" -ForegroundColor Yellow $syncSummary.FoldersProcessed++ # Read folder metadata $folderMetadataPath = Join-Path $folderDir.FullName ".folder.json" if (Test-Path $folderMetadataPath) { $folderMetadata = Get-Content $folderMetadataPath | ConvertFrom-Json Write-Verbose "Loaded folder metadata: ID $($folderMetadata.id), Name: $($folderMetadata.name)" } else { Write-Warning "Folder metadata file not found: $folderMetadataPath" continue } # Get all article JSON files in this folder $articleJsonFiles = Get-ChildItem -Path $folderDir.FullName -Filter "*.json" | Where-Object { $_.Name -ne ".folder.json" } foreach ($jsonFile in $articleJsonFiles) { $articleBaseName = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) $htmlFile = Join-Path $folderDir.FullName "$articleBaseName.html" Write-Host " Processing article: $articleBaseName" -ForegroundColor Cyan try { # Read article metadata and content $articleMetadata = Get-Content $jsonFile.FullName | ConvertFrom-Json if (Test-Path $htmlFile) { $articleContent = Get-Content $htmlFile -Raw } else { Write-Warning "HTML file not found for article: $htmlFile" $articleContent = "" } # Check if article exists in FreshService if ($articleMetadata.id -and $articleMetadata.id -gt 0) { # Update existing article if ($PSCmdlet.ShouldProcess("Article ID $($articleMetadata.id): $($articleMetadata.title)", "Update FreshService Article")) { # Get current article from FreshService to compare try { $currentArticle = Get-FsArticle -ID $articleMetadata.id # Check if update is needed (compare timestamps or force) $needsUpdate = $Force if (-not $needsUpdate) { $fileModified = (Get-Item $htmlFile -ErrorAction SilentlyContinue).LastWriteTime $fsModified = [datetime]$currentArticle.updated_at $needsUpdate = $fileModified -gt $fsModified } if ($needsUpdate) { Write-Verbose "Updating article ID $($articleMetadata.id)" # Update the article $updateParams = @{ ID = $articleMetadata.id Title = $articleMetadata.title Description = $articleContent } # Add optional parameters if present if ($articleMetadata.status) { $updateParams.Status = $articleMetadata.status } if ($articleMetadata.keywords) { $updateParams.Keywords = $articleMetadata.keywords } if ($articleMetadata.tags) { $updateParams.Tags = $articleMetadata.tags } Update-FsArticle @updateParams $syncSummary.ArticlesUpdated++ Write-Host " Updated article ID $($articleMetadata.id)" -ForegroundColor Green } else { Write-Verbose "Article ID $($articleMetadata.id) is up to date, skipping" $syncSummary.ArticlesSkipped++ } } catch { Write-Warning "Failed to update article ID $($articleMetadata.id): $_" $syncSummary.Errors += "Update article ID $($articleMetadata.id): $_" } } } else { # Create new article if ($PSCmdlet.ShouldProcess("New article: $($articleMetadata.title)", "Create FreshService Article")) { try { Write-Verbose "Creating new article: $($articleMetadata.title)" $createParams = @{ Title = $articleMetadata.title Description = $articleContent FolderID = $folderMetadata.id } # Add optional parameters if present if ($articleMetadata.status) { $createParams.Status = $articleMetadata.status } if ($articleMetadata.keywords) { $createParams.Keywords = $articleMetadata.keywords } if ($articleMetadata.tags) { $createParams.Tags = $articleMetadata.tags } if ($articleMetadata.article_type) { $createParams.ArticleType = $articleMetadata.article_type } $newArticle = New-FsArticle @createParams # Update the JSON file with the new ID $articleMetadata.id = $newArticle.id $articleMetadata.created_at = $newArticle.created_at $articleMetadata.updated_at = $newArticle.updated_at $articleMetadata | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonFile.FullName -Encoding UTF8 $syncSummary.ArticlesCreated++ Write-Host " Created article ID $($newArticle.id)" -ForegroundColor Green } catch { Write-Warning "Failed to create article '$($articleMetadata.title)': $_" $syncSummary.Errors += "Create article '$($articleMetadata.title)': $_" } } } } catch { Write-Warning "Failed to process article file '$($jsonFile.Name)': $_" $syncSummary.Errors += "Process article '$($jsonFile.Name)': $_" } } } } # Update final summary $syncSummary.EndTime = Get-Date $syncSummary.Duration = $syncSummary.EndTime - $syncSummary.StartTime Write-Host "`nSync completed!" -ForegroundColor Green Write-Host "Categories processed: $($syncSummary.CategoriesProcessed)" -ForegroundColor White Write-Host "Folders processed: $($syncSummary.FoldersProcessed)" -ForegroundColor White Write-Host "Articles created: $($syncSummary.ArticlesCreated)" -ForegroundColor White Write-Host "Articles updated: $($syncSummary.ArticlesUpdated)" -ForegroundColor White Write-Host "Articles skipped: $($syncSummary.ArticlesSkipped)" -ForegroundColor White Write-Host "Duration: $($syncSummary.Duration.ToString('mm\:ss'))" -ForegroundColor White if ($syncSummary.Errors.Count -gt 0) { Write-Host "Errors: $($syncSummary.Errors.Count)" -ForegroundColor Red $syncSummary.Errors | ForEach-Object { Write-Host " $_" -ForegroundColor Red } } return [PSCustomObject]$syncSummary } catch { Write-Error "Sync failed: $_" throw } } End { Write-Verbose -Message "Ending $($MyInvocation.InvocationName)..." } } |