Public/Copy-SPSiteStructure.ps1
|
function Copy-SPSiteStructure { <# .SYNOPSIS Copie la structure d'un site (colonnes, types de contenu, listes, vues, navigation). .DESCRIPTION Les travaux de copie de contenu supposent que la destination possède déjà le bon schéma : sans les colonnes de site et les types de contenu, les métadonnées n'ont nulle part où atterrir et `AllowSchemaMismatch` ne fait que masquer le problème. Cette commande enveloppe `Get-PnPSiteTemplate` / `Invoke-PnPSiteTemplate` du moteur de provisionnement PnP, avec la résilience et la journalisation du reste du module. À exécuter **avant** `Invoke-SPMigration`. .PARAMETER SourceSiteUrl Site dont la structure est extraite. .PARAMETER TargetSiteUrl Site où la structure est appliquée. Omettre pour n'extraire que le modèle. .PARAMETER TemplatePath Fichier .xml ou .pnp du modèle. Généré s'il n'existe pas. .PARAMETER Handlers Sous-ensemble d'éléments à traiter (Fields, ContentTypes, Lists, Navigation, PageContents…). Par défaut : schéma uniquement, sans contenu. .EXAMPLE # Extraire puis appliquer Copy-SPSiteStructure -SourceSiteUrl https://contoso.sharepoint.com/sites/A ` -TargetSiteUrl https://contoso.sharepoint.com/sites/B ` -TemplatePath ./modele-A.xml -WhatIf .EXAMPLE # Extraire seulement, pour revue avant application Copy-SPSiteStructure -SourceSiteUrl https://contoso.sharepoint.com/sites/A ` -TemplatePath ./modele-A.xml #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][string]$SourceSiteUrl, [string]$TargetSiteUrl, [Parameter(Mandatory)][string]$TemplatePath, [ValidateSet('Fields', 'ContentTypes', 'Lists', 'Navigation', 'SiteSecurity', 'TermGroups', 'CustomActions', 'Features', 'RegionalSettings', 'SupportedUILanguages', 'PropertyBagEntries', 'Pages', 'PageContents')] [string[]]$Handlers = @('Fields', 'ContentTypes', 'Lists', 'Navigation'), [switch]$ReuseExistingTemplate, # Auto : tente le modèle, et bascule sur la création bibliothèque par # bibliothèque si le service refuse. Invoke-PnPSiteTemplate tente des # opérations de niveau LOCATAIRE et échoue en « Access denied » sur un # compte pourtant administrateur des deux sites — sans repli, tout le # reste de la migration devient sans objet. [ValidateSet('Auto', 'Template', 'PerLibrary')] [string]$Method = 'Auto', [string]$ClientId = $env:SPM_CLIENT_ID, [string]$Tenant = $env:SPM_TENANT, [string]$Thumbprint = $env:SPM_CERT_THUMBPRINT ) $connectSplat = @{ ClientId = $ClientId } if ($Thumbprint) { if (-not $Tenant) { throw 'Tenant est obligatoire avec un certificat.' } $connectSplat.Thumbprint = $Thumbprint $connectSplat.Tenant = $Tenant } else { $connectSplat.Interactive = $true } $extracted = $false # ------------------------------------------------------------- extraction if ($ReuseExistingTemplate -and (Test-Path -LiteralPath $TemplatePath)) { Write-SPMLog -Level INFO -Operation 'Structure' -Message "Modèle existant réutilisé : $TemplatePath" } else { $srcConn = Connect-SPMSite @connectSplat -Url $SourceSiteUrl $dir = Split-Path -Parent $TemplatePath if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } Write-SPMLog -Level INFO -Operation 'Structure' -Message ( "Extraction du modèle de $SourceSiteUrl (handlers : $($Handlers -join ', '))" ) Invoke-SPMWithRetry -Operation 'Get-PnPSiteTemplate' -ScriptBlock { Get-PnPSiteTemplate -Out $TemplatePath -Handlers $Handlers -Force ` -Connection $srcConn -ErrorAction Stop } | Out-Null $extracted = $true Write-SPMLog -Level SUCCESS -Operation 'Structure' -Message "Modèle extrait : $TemplatePath" } if (-not $TargetSiteUrl) { return [pscustomobject]@{ Extracted = $extracted Applied = $false TemplatePath = (Resolve-Path $TemplatePath).Path Handlers = $Handlers } } # ------------------------------------------------------------ application if (-not $PSCmdlet.ShouldProcess($TargetSiteUrl, "Appliquer le modèle $([System.IO.Path]::GetFileName($TemplatePath))")) { return [pscustomobject]@{ Extracted = $extracted Applied = $false Simulated = $true TemplatePath = (Resolve-Path $TemplatePath).Path Handlers = $Handlers } } $dstConn = Connect-SPMSite @connectSplat -Url $TargetSiteUrl $schemaResult = $null $methodUsed = $Method if ($Method -ne 'PerLibrary') { Write-SPMLog -Level INFO -Operation 'Structure' -Message "Application du modèle sur $TargetSiteUrl" try { Invoke-SPMWithRetry -Operation 'Invoke-PnPSiteTemplate' -ScriptBlock { Invoke-PnPSiteTemplate -Path $TemplatePath -Handlers $Handlers ` -Connection $dstConn -ErrorAction Stop } | Out-Null $methodUsed = 'Template' Write-SPMLog -Level SUCCESS -Operation 'Structure' -Message "Structure appliquée sur $TargetSiteUrl" } catch { $message = ($_.Exception.Message -split [char]10)[0] if ($Method -eq 'Template') { throw } Write-SPMLog -Level WARNING -Operation 'Structure' -Message ( "Provisionnement refusé, bascule sur la création bibliothèque par bibliothèque : $message" ) $methodUsed = 'PerLibrary' } } if ($methodUsed -eq 'PerLibrary') { # Le repli ne couvre PAS ce que couvre un modèle : navigation, sécurité, # pages, entrées de sac de propriétés restent à la charge de l'opérateur. # Il couvre ce sans quoi aucun contenu ne peut atterrir : les # bibliothèques et leurs colonnes. Write-SPMLog -Level WARNING -Operation 'Structure' -Message ( 'Repli PerLibrary : bibliothèques et colonnes seulement. ' + 'Navigation, sécurité et pages ne sont PAS reprises.' ) $schemaSplat = @{ SourceSiteUrl = $SourceSiteUrl TargetSiteUrl = $TargetSiteUrl ClientId = $ClientId } if ($Thumbprint) { $schemaSplat.Thumbprint = $Thumbprint; $schemaSplat.Tenant = $Tenant } $schemaResult = Copy-SPSiteSchema @schemaSplat } [pscustomobject]@{ Extracted = $extracted Applied = $true Simulated = $false Method = $methodUsed Schema = $schemaResult TemplatePath = (Resolve-Path $TemplatePath).Path Handlers = $Handlers Target = $TargetSiteUrl } } |