Public/Export-FsKnowledgeBase.ps1
Function Export-FsKnowledgeBase { <# .SYNOPSIS Exports the complete FreshService knowledge base to a local folder structure .DESCRIPTION The Export-FsKnowledgeBase function exports all categories, folders, and articles from your FreshService domain to a structured folder hierarchy with separate HTML content files and JSON metadata files. This creates a git-friendly representation of your knowledge base. .EXAMPLE Export-FsKnowledgeBase -OutputPath "C:\MyRepo\Articles" Exports the entire knowledge base to the specified folder .EXAMPLE Export-FsKnowledgeBase -OutputPath "C:\MyRepo\Articles" -CategoryID 123 Exports only the specified category and its contents .PARAMETER OutputPath The local folder path where the knowledge base should be exported .PARAMETER CategoryID Optional: Export only a specific category (exports all categories if not specified) .PARAMETER Force Overwrites existing files without prompting .INPUTS None .OUTPUTS [PSCustomObject] - Returns export summary with counts of categories, folders, and articles exported .NOTES Requires FreshService API authentication Creates 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]$OutputPath, [Parameter(Mandatory=$false)] [Int64]$CategoryID, [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 $exportSummary = @{ CategoriesExported = 0 FoldersExported = 0 ArticlesExported = 0 Errors = @() StartTime = Get-Date } } Process { try { # Create output directory if it doesn't exist if (-not (Test-Path $OutputPath)) { Write-Verbose "Creating output directory: $OutputPath" New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } # Get categories to export if ($CategoryID) { Write-Verbose "Exporting specific category: $CategoryID" $categories = @(Get-FsArticleCategory -ID $CategoryID) } else { Write-Verbose "Exporting all categories" $categories = Get-FsArticleCategory } foreach ($category in $categories) { Write-Host "Exporting category: $($category.name)" -ForegroundColor Green # Create category folder with safe name $categoryFolderName = ConvertTo-SafeFolderName -Name $category.name $categoryPath = Join-Path $OutputPath $categoryFolderName if (-not (Test-Path $categoryPath)) { New-Item -ItemType Directory -Path $categoryPath -Force | Out-Null } # Export category metadata $categoryMetadata = $category | Select-Object * -ExcludeProperty CategoryName, CategoryID $categoryMetadataPath = Join-Path $categoryPath ".category.json" $categoryMetadata | ConvertTo-Json -Depth 10 | Out-File -FilePath $categoryMetadataPath -Encoding UTF8 Write-Verbose "Exported category metadata: $categoryMetadataPath" $exportSummary.CategoriesExported++ # Get folders in this category try { $folders = Get-FsArticleFolder -CategoryID $category.id foreach ($folder in $folders) { Write-Host " Exporting folder: $($folder.name)" -ForegroundColor Yellow # Create folder directory with safe name $folderName = ConvertTo-SafeFolderName -Name $folder.name $folderPath = Join-Path $categoryPath $folderName if (-not (Test-Path $folderPath)) { New-Item -ItemType Directory -Path $folderPath -Force | Out-Null } # Export folder metadata $folderMetadata = $folder | Select-Object * -ExcludeProperty CategoryName, CategoryID, FolderName, FolderID $folderMetadataPath = Join-Path $folderPath ".folder.json" $folderMetadata | ConvertTo-Json -Depth 10 | Out-File -FilePath $folderMetadataPath -Encoding UTF8 Write-Verbose "Exported folder metadata: $folderMetadataPath" $exportSummary.FoldersExported++ # Get articles in this folder try { $articles = Get-FsArticle -FolderID $folder.id foreach ($article in $articles) { Write-Host " Exporting article: $($article.title)" -ForegroundColor Cyan # Create safe filename from article title $articleFileName = ConvertTo-SafeFileName -Name $article.title # Export article HTML content $htmlPath = Join-Path $folderPath "$articleFileName.html" if ($Force -or -not (Test-Path $htmlPath) -or $PSCmdlet.ShouldProcess($htmlPath, "Export article HTML")) { $article.description | Out-File -FilePath $htmlPath -Encoding UTF8 Write-Verbose "Exported article HTML: $htmlPath" } # Export article metadata (excluding content) $articleMetadata = $article | Select-Object * -ExcludeProperty description, CategoryName, CategoryID, FolderName, FolderID $jsonPath = Join-Path $folderPath "$articleFileName.json" if ($Force -or -not (Test-Path $jsonPath) -or $PSCmdlet.ShouldProcess($jsonPath, "Export article metadata")) { $articleMetadata | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonPath -Encoding UTF8 Write-Verbose "Exported article metadata: $jsonPath" } $exportSummary.ArticlesExported++ } } catch { $errorMsg = "Failed to export articles from folder '$($folder.name)': $_" Write-Warning $errorMsg $exportSummary.Errors += $errorMsg } } } catch { $errorMsg = "Failed to export folders from category '$($category.name)': $_" Write-Warning $errorMsg $exportSummary.Errors += $errorMsg } } # Update final summary $exportSummary.EndTime = Get-Date $exportSummary.Duration = $exportSummary.EndTime - $exportSummary.StartTime Write-Host "`nExport completed!" -ForegroundColor Green Write-Host "Categories: $($exportSummary.CategoriesExported)" -ForegroundColor White Write-Host "Folders: $($exportSummary.FoldersExported)" -ForegroundColor White Write-Host "Articles: $($exportSummary.ArticlesExported)" -ForegroundColor White Write-Host "Duration: $($exportSummary.Duration.ToString('mm\:ss'))" -ForegroundColor White if ($exportSummary.Errors.Count -gt 0) { Write-Host "Errors: $($exportSummary.Errors.Count)" -ForegroundColor Red $exportSummary.Errors | ForEach-Object { Write-Host " $_" -ForegroundColor Red } } return [PSCustomObject]$exportSummary } catch { Write-Error "Export failed: $_" throw } } End { Write-Verbose -Message "Ending $($MyInvocation.InvocationName)..." } } Function ConvertTo-SafeFolderName { [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [string]$Name ) # Remove invalid characters and replace with underscores $safeName = $Name -replace '[<>:"/\\|?*]', '_' # Remove multiple consecutive underscores $safeName = $safeName -replace '_{2,}', '_' # Remove leading/trailing underscores $safeName = $safeName.Trim('_') # Limit length if ($safeName.Length -gt 100) { $safeName = $safeName.Substring(0, 100).TrimEnd('_') } return $safeName } Function ConvertTo-SafeFileName { [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [string]$Name ) # Remove invalid characters and replace with underscores $safeName = $Name -replace '[<>:"/\\|?*]', '_' # Remove multiple consecutive underscores $safeName = $safeName -replace '_{2,}', '_' # Remove leading/trailing underscores $safeName = $safeName.Trim('_') # Limit length (accounting for .html/.json extensions) if ($safeName.Length -gt 200) { $safeName = $safeName.Substring(0, 200).TrimEnd('_') } return $safeName } |