Functions/Backup-BsgPbiWorkspace.ps1

<#
    .SYNOPSIS
        Backup a Power BI workspace to a local directory.
         
    .DESCRIPTION
        The workspace objects and definitions are saved to files in a local directory.
 
    .PARAMETER Source_WorkspaceName
        The name of the workspace you would like to backup.
  
    .PARAMETER Path
        The path to the root folder, where the temporary files will be saved.
        Subfolders will be created automatically.
 
    .PARAMETER PbiConnection
        The connection to the Power BI Tenant.
 
    .PARAMETER MetadataOnly
        Backup only Metadata excluding actual Dataset files (PBIX Files).
 
    .EXAMPLE
        # Backup workspace
        Backup-BsgPbiWorkspace -Source_WorkspaceName "BSGroup DA - Test Workspace" -Path "C:\temp\BSG PBI Administration"
         
    .EXAMPLE
        # Backup personal workspace
        Backup-BsgPbiWorkspace -Source_WorkspaceName "My Workspace" -Path "C:\temp\BSG PBI Administration"
 
    .INPUTS
 
    .OUTPUTS
 
    .NOTES
        This script uses the Power BI Management module for Windows PowerShell.
        If this module is not installed, install it by using the command 'Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser'.
#>


function Backup-BsgPbiWorkspace{

    param
    (
        [Parameter(Mandatory=$true)][string]$Source_WorkspaceName
        ,[Parameter(Mandatory=$true)][string]$Path
        ,[Parameter(Mandatory=$false)][Microsoft.PowerBI.Common.Abstractions.Interfaces.IPowerBIProfile] $PbiConnection = $null
        ,[Parameter(Mandatory=$false)][switch]$MetadataOnly = $false
    )

    try {

        # Info message
        Write-Host
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor white -String "---------------------------------------------------------------------------------------------"
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor white -String " Backup workspace... "
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor gray -String " Name: <c='green'>$Source_WorkspaceName</c>"
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor white -String "---------------------------------------------------------------------------------------------"
        Write-Host

        # PBI connection
        if ($null -eq $PbiConnection){
            Write-Host
            Write-PSFHostColor -Level Host -DefaultColor gray -String "Choose the tenant and user to start the backup..."
            Write-PSFHostColor -Level Host -DefaultColor gray -String "You need to be a <c='white'>Power BI Administrator</c> to backup a workspace."
            try{
                Logout-PowerBI
                $PbiLogin = Login-PowerBI -ErrorAction Stop
            } catch{
                throw "Error trying to connect to Power BI tenant."
            }
            $TenantId = $PbiLogin.TenantId
            $UserName = $PbiLogin.UserName
            Write-Host
            Write-PSFHostColor -Level Host -DefaultColor gray -String "You are logged in as user <c='white'>$userName</c> in tenant <c='white'>$TenantId</c>." 
            Write-PSFHostColor -Level Host -DefaultColor gray -String "<c='white'>Would you like to start the backup? [y/n]: </c>"
            $confirmTenant = Read-Host
            Write-Host
            if ($confirmTenant -eq 'y'){
                # continue...
            } else{
                Write-PSFHostColor -Level Host -DefaultColor gray -String "Backup stopped."
                Exit
            }
        }


        # ===
        # Get workspace
        # =

        try{

            if ($Source_WorkspaceName -in 'Mein Arbeitsbereich', 'Personal Workspace', 'My Workspace',
                                          'mein arbeitsbereich', 'personal workspace', 'my workspace'){
                $Source_WorkspaceName = 'My Workspace'
                $Source_WorkspaceId = 'me'
            } else{
                $Source_Workspace = Get-PowerBIWorkspace -Name $Source_WorkspaceName
                if (!$Source_Workspace){
                    throw "Ensure that the workspace exists."
                }
                $Source_WorkspaceId = $Source_Workspace.id
            }

            
        } catch{
            throw "Error trying to get Power BI workspace. $_"
        }


        # Create base paths and filenames
        $Path_Backup = Join-Path -Path $Path -ChildPath "Backup"
        $Path_Workspaces = Join-Path -Path $Path_Backup -ChildPath "Workspaces"
        $Path_Workspace = Join-Path -Path $Path_Workspaces -ChildPath $Source_WorkspaceName
        $Path_Datasets = Join-Path -Path $Path_Workspace -ChildPath "Datasets"
        $Path_Reports = Join-Path -Path $Path_Workspace -ChildPath "Reports"
        $FileName_ReportDatasetMapping = "Mapping_ReportDataset.json"
        $FileName_WorkspaceMapping = "Mapping_Workspace.json"
        $Path_ReportDatasetMapping = Join-Path -Path $Path_Workspace -ChildPath $FileName_ReportDatasetMapping
        # $Path_WorkspaceMapping = Join-Path -Path $Path_Workspace -ChildPath $FileName_WorkspaceMapping


        # ===
        # Delete old workspace backup if exists?
        # =
        if (Test-Path $Path_Workspace){
            Write-Host
            Write-Warning "A backup for workspace `"$Source_WorkspaceName`" already exists."
            Write-Host
            Write-PSFHostColor -Level Host -DefaultColor gray -String "Location: <c='white'>$Path_Workspace</c>"
            Write-PSFHostColor -Level Host -DefaultColor gray -String "It is recommended to delete or rename the old backup."
            Write-Host
            $confirmation = Read-Host "Would you like to delete the old workspace backup? [y/n]"
            Write-Host
            if ($confirmation -eq 'y'){
                Remove-Item -Path $Path_Workspace -Force -Recurse
            } else{
                Write-PSFHostColor -Level Host -DefaultColor gray -String "Backup stopped."
                Exit
            }
        }


        # ===
        # Export workspace
        # =

        # Export Workspace metadata
        Export-BsgPbiWorkspaceMetadata -Source_WorkspaceId $Source_WorkspaceId -Path_Workspace $Path_Workspace
        

        # ===
        # Export dataset
        # =

        # Define base URLs and path
        if ($Source_WorkspaceId -eq 'me'){
            $Source_WorkspaceUrl = "https://api.powerbi.com/v1.0/myorg"
        } else{
            $Source_WorkspaceUrl = "https://api.powerbi.com/v1.0/myorg/groups/" + $Source_WorkspaceId
        }
        $Source_DatasetUrl = $Source_WorkspaceUrl + "/datasets"
        $RequestUrl = $Source_DatasetUrl
        $Path_Datasets = Join-Path -Path $Path_Workspace -ChildPath "Datasets"

        # Get datasets (API-Request)
        try{
            $Source_Datasets = Invoke-PowerBIRestMethod -Url $RequestUrl -Method Get | ConvertFrom-Json
        } catch{
            throw "Error after calling request URL: `"$RequestUrl`"."
        }

        # Export each dataset
        Foreach ($dataset in $Source_Datasets.value) {
            $Source_DatasetId = [guid]$dataset.id
            Export-BsgPbiDataset -Source_WorkspaceId $Source_WorkspaceId -Source_DatasetId $Source_DatasetId -Path_Workspace $Path_Workspace
        }


        # ===
        # Export reports and datasets
        # =

        # Keep track of the report and dataset IDs
        $Mapping_ReportDataset = @()

        # Get the reports from workspace
        $reports = if ($Source_WorkspaceId -eq "me") { Get-PowerBIReport } else { Get-PowerBIReport -WorkspaceId $Source_WorkspaceId }

        # ===
        # Export reports (pbix) and create mapping file
        # =
        Foreach ($report in $reports) {

            $Source_ReportId = [guid]$report.id
            $Source_DatasetId = [guid]$report.datasetId
            $Source_ReportName = $report.name

            # Check if report and dataset have different name
            $Source_Dataset = $Source_Datasets.value | Where-Object {$_.id -eq $Source_DatasetId}
            if ($Source_Dataset -eq $null){
                $datasetIsInDifferentWorkspace = $true
                $datasetHasDifferentName = $true
                $Target_DatasetName = '' # dataset is in other workspace
            } else {
                $datasetIsInDifferentWorkspace = $false
                if ($Source_ReportName -eq $Source_Dataset.name){
                    $datasetHasDifferentName = $false
                    $Target_DatasetName = $Source_ReportName
                } else {
                    $datasetHasDifferentName = $true
                    $Target_DatasetName = $Source_Dataset.name
                }
            }

            # Export report
            $ReportExported = Export-BsgPbiReport -Source_WorkspaceId $Source_WorkspaceId -Source_ReportId $Source_ReportId -Path_Workspace $Path_Workspace -MetadataOnly:$MetadataOnly.IsPresent

            if ($ReportExported -and !$MetadataOnly.IsPresent){
                # Get report connection
                $Filename = $Source_ReportName + "_" + $Source_ReportId
                $PbiReportConnection = Get-BsgPbiReportConnection -Filename $Filename -Path $Path_Reports

                # find reports with shared datasets
                # If report is using a PBI Service dataset (Live connection): mark with 'isUsingSharedDataset = true'
                if ($PbiReportConnection.Connections.ConnectionType -eq 'pbiServiceLive'){
                    $isUsingSharedDataset = $true
                } else {
                    $isUsingSharedDataset = $false
                }

                # define dataset owner and restore action
                # If multiple reports are assossiated with a dataset: Mark the report with the same name as dataset owner, otherwise mark the first one.
                if ($datasetIsInDifferentWorkspace){
                    $restoreAction = 'Wait_UpdatePbiConnection'
                    $restoreDescription = 'Wait until all workspace have been processed, then update the report connection and import the report without dataset.'
                } else {
                    if ($isUsingSharedDataset){
                        $isDatasetOwner = $false
                        $restoreAction = 'UpdatePbiConnection'
                        $restoreDescription = 'Update the report connection and reestablish connection.'
                    } else {
                        $OwnerAlreadyExists = @($Mapping_ReportDataset | Where-Object {$_.datasetId -eq $Source_DatasetId -and $_.isDatasetOwner -eq $true}).Count -ge 1
                        if ($OwnerAlreadyExists){
                            $isDatasetOwner = $false
                            $restoreAction = 'RebindReport'
                            $restoreDescription = 'Rebind the report to another dataset.'
                        } elseif ($datasetHasDifferentName) {
                            $isDatasetOwner = $true
                            $restoreAction = 'CreateDataset_RebindReport'
                            $restoreDescription = 'Create the dataset and rebind the report.'
                        } else{
                            $isDatasetOwner = $true
                            $restoreAction = 'CreateDataset'
                            $restoreDescription = 'Create the dataset.'
                        }
                    }
                }
            } else {
                $isDatasetOwner = $false
                if ($Source_ReportName -eq "Report Usage Metrics Report"){
                    $restoreAction = 'Skip_UsageMetrics'
                    $restoreDescription = 'Report was skipped (not exported). The Usage Metric Report can be recreated in new workspace.'
                } else{
                    $restoreAction = 'Skip'
                    $restoreDescription = 'Report was skipped (not exported).'
                }
            }


            # add mapping object
            $Mapping_ReportDataset += [PSCustomObject]@{
                reportId = $Source_ReportId
                reportName = $Source_ReportName
                datasetId = $Source_DatasetId
                isDatasetOwner = $isDatasetOwner
                isUsingSharedDataset = $isUsingSharedDataset
                datasetIsInDifferentWorkspace = $datasetIsInDifferentWorkspace
                datasetHasDifferentName = $datasetHasDifferentName
                restoreAction = $restoreAction
                restoreDescription = $restoreDescription
                new_reportName = $Source_ReportName
                new_datasetName = $Target_DatasetName
            }

        }
        
        # ===
        # Save dataset and report mapping in a JSON file
        # =

        $Mapping_ReportDataset | ConvertTo-Json -depth 1 | Out-File -FilePath $Path_ReportDatasetMapping

        if ($Mapping_ReportDataset.restoreAction -contains 'Skip'){
            Write-Host
            Write-Warning "At least one report was skipped."
            Write-Host
            Write-PSFHostColor -Level Host -DefaultColor gray -String "<c='white'>Info</c>: Reports marked as <c='white'>restoreAction = 'Skip'</c> in the mapping file will not be restored."
        }


        # Info message
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor white -String "---------------------------------------------------------------------------------------------"
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor gray -String " <c='green'>Workspace backup finished.</c>"
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor gray -String " Name: <c='white'>$Source_WorkspaceName</c>"
        Write-PSFHostColor -Level Host -DefaultColor gray -String " Location: <c='white'>$Path_Workspace</c>"
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor gray -String " <c='white'>Info</c>: To change the name of the workspace or a report, you can edit following mapping files:"
        Write-PSFHostColor -Level Host -DefaultColor gray -String " Workspace: <c='white'>$FileName_WorkspaceMapping</c>"
        Write-PSFHostColor -Level Host -DefaultColor gray -String " Reports: <c='white'>$FileName_ReportDatasetMapping</c>"
        Write-Host
        Write-PSFHostColor -Level Host -DefaultColor gray -String " Developed by <c='white'>BSGroup Data Analytics AG</c>"
        Write-PSFHostColor -Level Host -DefaultColor white -String "---------------------------------------------------------------------------------------------"
        Write-Host
        Write-Host
        
    }
    catch {

        if ($Source_WorkspaceName){
            Write-Host
            Stop-PSFFunction -Message "Could not export workspace `"$Source_WorkspaceName`"." -EnableException $False -Errorrecord $_
            return
        } else{
            Write-Host
            Stop-PSFFunction -Message "Could not export workspace `"$Source_WorkspaceId`"." -EnableException $False -Errorrecord $_
            return
        }
        
    }

}