function Export-Tableau { <# .SYNOPSIS Exports the workbook XML to a TWB or TWBX file. .PARAMETER Path The literal file path to export to. .PARAMETER WorkbookXml The workbook XML to export. .PARAMETER Update Whether to update the TWB inside the destination TWBX file if the destination file exists. .PARAMETER Force Whether to overwrite the destination TWBX file if it exists. By default, you will be prompted whether to overwrite any existing file. .NOTES Author: Joshua Poehls #> [CmdletBinding( SupportsShouldProcess=$true )] param( [Parameter( Position = 0, Mandatory = $true )] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter( Position = 1, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [ValidateNotNullOrEmpty()] [xml]$WorkbookXml, [switch]$Update, [switch]$Force ) begin { # System.IO.Compression.FileSystem requires at least .NET 4.5 Add-Type -AssemblyName "System.IO.Compression" } process { $Path = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) $entryName = [System.IO.Path]::GetFileNameWithoutExtension($Path) + '.twb' $createNewTwbx = $false if (Test-Path $Path) { if ($Update -or $Force -or $PSCmdlet.ShouldContinue('Overwrite existing file?', 'Confirm')) { if ($Update) { if ($PSCmdlet.ShouldProcess($Path, 'Update TWB in packaged workbook')) { [System.IO.FileStream]$fileStream = $null [System.IO.Compression.ZipArchive]$zip = $null try { $fileStream = New-Object System.IO.FileStream -ArgumentList $Path, ([System.IO.FileMode]::Open), ([System.IO.FileAccess]::ReadWrite), ([System.IO.FileShare]::Read) $zip = New-Object System.IO.Compression.ZipArchive -ArgumentList $fileStream, ([System.IO.Compression.ZipArchiveMode]::Update) # Locate the existing TWB entry and remove it. $entry = $zip.Entries | where { # Look for a .twb file at the root level of the archive. $_.FullName -eq $_.Name -and ([System.IO.Path]::GetExtension($_.Name)) -eq '.twb' } | select -First 1 if ($entry) { $entry.Delete() } $entry = $zip.CreateEntry($entryName, ([System.IO.Compression.CompressionLevel]::Optimal)) [System.IO.Stream]$entryStream = $null try { $entryStream = $entry.Open() $WorkbookXml.Save($entryStream) } finally { if ($entryStream) { $entryStream.Dispose() } } } finally { if ($zip) { $zip.Dispose() } if ($fileStream) { $fileStream.Dispose() } } } } else { if ($PSCmdlet.ShouldProcess($Path, 'Replace existing packaged workbook')) { # delete existing TWBX Remove-Item $Path -ErrorAction Stop #TODO: Figure out how to pass WhatIf and Confirm to this $createNewTwbx = $true } } } } else { if ($PSCmdlet.ShouldProcess($Path, 'Export packaged workbook')) { $createNewTwbx = $true } } if ($createNewTwbx) { [System.IO.FileStream]$fileStream = $null [System.IO.Compression.ZipArchive]$zip = $null try { $fileStream = New-Object System.IO.FileStream -ArgumentList $Path, ([System.IO.FileMode]::CreateNew), ([System.IO.FileAccess]::ReadWrite), ([System.IO.FileShare]::None) $zip = New-Object System.IO.Compression.ZipArchive -ArgumentList $fileStream, ([System.IO.Compression.ZipArchiveMode]::Update) $entry = $zip.CreateEntry($entryName, ([System.IO.Compression.CompressionLevel]::Optimal)) [System.IO.Stream]$entryStream = $null try { $entryStream = $entry.Open() $WorkbookXml.Save($entryStream) } finally { if ($entryStream) { $entryStream.Dispose() } } } finally { if ($zip) { $zip.Dispose() } if ($fileStream) { $fileStream.Dispose() } } } } } ?function Get-TableauObject { <# .SYNOPSIS Gets a summary object for a workbook. .NOTES Author: Joshua Poehls #> [CmdletBinding()] param( [Parameter( Mandatory = $true, ParameterSetName = "Xml", Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [xml[]]$WorkbookXml, [Parameter( Mandatory = $true, ParameterSetName = "Path", Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string[]]$Path, [Parameter( Mandatory = $true, ParameterSetName = "LiteralPath", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias("FullName")] [ValidateNotNullOrEmpty()] [string[]]$LiteralPath ) process { $needXml = $false if ($PSCmdlet.ParameterSetName -eq "Path") { $paths = Resolve-Path -Path $Path | select -ExpandProperty Path $needXml = $true } elseif ($PSCmdlet.ParameterSetName -eq "LiteralPath") { $paths = Resolve-Path -LiteralPath $LiteralPath | select -ExpandProperty Path $needXml = $true } if ($needXml) { $WorkbookXml = $paths | foreach { Get-TableauXml $_ } } $i = 0 foreach ($xml in $WorkbookXml) { $worksheets = @() $xml | Select-Xml '/workbook/worksheets/worksheet' | select -ExpandProperty Node | foreach { $props = @{ "Name" = $_.Attributes['name'].Value; "DisplayName" = if ($_.Attributes['caption']) { $_.Attributes['caption'].Value } else { $_.Attributes['name'].Value }; # TODO: Include list of referenced data sources. }; $worksheets += New-Object PSObject -Property $props } $dashboards = @() $xml | Select-Xml '/workbook/dashboards/dashboard' | select -ExpandProperty Node | foreach { # TODO: This really slows down the whole cmdlet. Find a way to make the dashboards' Worksheets property lazy evaluated. $dashboardWorksheets = @() $_ | Select-Xml './zones//zone' | select -ExpandProperty Node | # Assume any zone with a @name but not a @type is a worksheet zone. where { $_.Attributes['type'] -eq $null -and $_.Attributes['name'] -ne $null } | foreach { $zone = $_ $dashboardWorksheets += ($worksheets | where { $_.Name -eq $zone.Attributes['name'].Value }) } $props = @{ "Name" = $_.Attributes['name'].Value; "DisplayName" = if ($_.Attributes['caption']) { $_.Attributes['caption'].Value } else { $_.Attributes['name'].Value }; "Worksheets" = $dashboardWorksheets; }; $dashboards += New-Object PSObject -Property $props } $dataSources = @() $xml | Select-Xml '/workbook/datasources/datasource' | select -ExpandProperty Node | foreach { $props = @{ "Name" = $_.Attributes['name'].Value; "DisplayName" = if ($_.Attributes['caption']) { $_.Attributes['caption'].Value } else { $_.Attributes['name'].Value }; "ConnectionType" = ($_ | Select-Xml './connection/@class').Node.Value; # TODO: Include a "Connection" PSObject property with properties specific to the type of connection (i.e. file path for CSVs and server for SQL Server, etc). }; $dataSources += New-Object PSObject -Property $props } $parameters = @() $xml | Select-Xml '/workbook/datasources/datasource[@name="Parameters"]/column' | select -ExpandProperty Node | foreach { $props = @{ "Name" = $_.Attributes['name'].Value -ireplace "^\[|\]$", ""; "DisplayName" = if ($_.Attributes['caption']) { $_.Attributes['caption'].Value } else { $_.Attributes['name'].Value -ireplace "^\[|\]$", "" }; "DataType" = $_.Attributes['datatype'].Value; "DomainType" = $_.Attributes['param-domain-type'].Value; "Value" = $_.Attributes['value'].Value; "ValueDisplayName" = if ($_.Attributes['alias']) { $_.Attributes['alias'].Value } else { $_.Attributes['value'].Value }; # TODO: For "list" parameters, include a ValueList property with all of the values and aliases. }; $parameters += New-Object PSObject -Property $props } $props = @{ "FileVersion" = ($xml | Select-Xml '/workbook/@version').Node.Value; "BuildVersion" = ($xml | Select-Xml '//comment()[1]').Node.Value -ireplace "[^\d\.]", ""; "Parameters" = $parameters; "DataSources" = $dataSources; "Worksheets" = $worksheets; "Dashboards" = $dashboards; "WorkbookXml" = $xml; } if ($paths) { $props['FileName'] = $paths | select -Index $i } Write-Output (New-Object PSObject -Property $props) $i++ } } } # Tests for the magic zip file header. # Inspired by http://stackoverflow.com/a/1887113/31308 function Test-ZipFile([string]$path) { try { $stream = New-Object System.IO.StreamReader -ArgumentList @($path) $reader = New-Object System.IO.BinaryReader -ArgumentList @($stream.BaseStream) $bytes = $reader.ReadBytes(4) if ($bytes.Length -eq 4) { if ($bytes[0] -eq 80 -and $bytes[1] -eq 75 -and $bytes[2] -eq 3 -and $bytes[3] -eq 4) { return $true; } } } finally { if ($reader) { $reader.Dispose(); } if ($stream) { $stream.Dispose(); } } return $false; } function Get-TableauXml { <# .SYNOPSIS Gets the workbook XML from a TWB or TWBX file. .NOTES Author: Joshua Poehls #> [CmdletBinding()] param( [Parameter( Mandatory = $true, ParameterSetName = "Path", Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string[]]$Path, [Parameter( Mandatory = $true, ParameterSetName = "LiteralPath", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias("FullName")] [string[]]$LiteralPath ) begin { # System.IO.Compression.FileSystem requires at least .NET 4.5 Add-Type -AssemblyName "System.IO.Compression" } process { if ($PSCmdlet.ParameterSetName -eq "Path") { $paths = Resolve-Path -Path $Path | select -ExpandProperty Path } elseif ($PSCmdlet.ParameterSetName -eq "LiteralPath") { $paths = Resolve-Path -LiteralPath $LiteralPath | select -ExpandProperty Path } foreach ($p in $paths) { #$extension = [System.IO.Path]::GetExtension($p) if (-not(Test-ZipFile $p)) { Write-Output ([xml](Get-Content -LiteralPath $p)) } else { $archiveStream = $null $archive = $null $reader = $null try { $archiveStream = New-Object System.IO.FileStream($p, [System.IO.FileMode]::Open) $archive = New-Object System.IO.Compression.ZipArchive($archiveStream) $twbEntry = ($archive.Entries | Where-Object { $_.FullName -eq $_.Name -and [System.IO.Path]::GetExtension($_.Name) -eq ".twb" })[0] $reader = New-Object System.IO.StreamReader $twbEntry.Open() [xml]$xml = $reader.ReadToEnd() Write-Output $xml } finally { if ($reader -ne $null) { $reader.Dispose() } if ($archive -ne $null) { $archive.Dispose() } if ($archiveStream -ne $null) { $archiveStream.Dispose() } } } } } } Export-ModuleMember -Function Get-TableauXml