Update application version to 1.3.4 and enhance support for SharePoint Subscription Edition in migration scripts and documentation

This commit is contained in:
Torsten Brendgen
2026-08-26 23:11:39 +02:00
parent 90d17be932
commit f179bed204
4 changed files with 141 additions and 172 deletions
+117 -151
View File
@@ -33,7 +33,7 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$script:ApplicationVersion = "1.2.0"
$script:ApplicationVersion = "1.3.4"
$script:SupportedMappingSchemaVersion = 2
$script:SkippedImportFieldWarnings = @{}
$script:ListImportTrackingFieldInternalName = "StartSPMigrationSourceUniqueId"
@@ -101,6 +101,7 @@ function Get-SharePointProductSupportInfo {
}
elseif ($BuildVersion.Major -eq 16 -and $BuildVersion.Build -ge 14326) {
$productName = "SharePoint Server Subscription Edition"
$isSupported = $true
}
return [PSCustomObject]@{
@@ -163,7 +164,7 @@ function Initialize-SharePointPowerShell {
$environment = Get-SharePointServerEnvironment
if (-not $environment.IsSupported) {
throw ("Nicht unterstuetzte lokale SharePoint-Farm: {0}, Build {1}. Unterstuetzt werden SharePoint Server 2016 und 2019." -f $environment.ProductName, $environment.BuildVersion)
throw ("Nicht unterstuetzte lokale SharePoint-Farm: {0}, Build {1}. Unterstuetzt werden SharePoint Server 2016, 2019 und Subscription Edition." -f $environment.ProductName, $environment.BuildVersion)
}
Write-Host ("SSOM-Umgebung: ToolVersion={0}; Produkt='{1}'; FarmBuild={2}; WindowsPowerShell={3}; 64Bit={4}" -f $script:ApplicationVersion, $environment.ProductName, $environment.BuildVersion, $environment.PowerShellVersion, $environment.Is64BitProcess)
@@ -222,8 +223,14 @@ function Invoke-MigrationImport {
Write-Host ("Migrationskompatibilitaet: Quelle='{0}' Build={1}; Ziel='{2}' Build={3}" -f $sourceSharePointProduct, $sourceSharePointBuild, $script:SharePointEnvironment.ProductName, $script:SharePointEnvironment.BuildVersion)
if ($sourceSharePointProduct -eq "SharePoint Server 2019" -and $script:SharePointEnvironment.ProductName -eq "SharePoint Server 2016") {
Write-Warning "Ein Import von SharePoint Server 2019 nach SharePoint Server 2016 ist ein nicht garantierter Downgrade. In 2019 vorhandene Feldtypen, Content-Types oder Listentemplates koennen im Ziel fehlen."
$productLevels = @{
"SharePoint Server 2016" = 1
"SharePoint Server 2019" = 2
"SharePoint Server Subscription Edition" = 3
}
if ($productLevels.ContainsKey($sourceSharePointProduct) -and $productLevels.ContainsKey($script:SharePointEnvironment.ProductName) -and $productLevels[$sourceSharePointProduct] -gt $productLevels[$script:SharePointEnvironment.ProductName]) {
Write-Warning ("Ein Import von '{0}' nach '{1}' ist ein nicht garantierter Downgrade. Neuere Feldtypen, Content-Types oder Listentemplates koennen im Ziel fehlen." -f $sourceSharePointProduct, $script:SharePointEnvironment.ProductName)
}
$shouldImportFiles = $ImportFiles.IsPresent
@@ -462,6 +469,31 @@ function Convert-SPFieldValue {
return $Value
}
function Convert-SPItemFieldValueForExport {
param(
[Parameter(Mandatory = $true)]
[Microsoft.SharePoint.SPField]$Field,
$Value
)
if ($null -ne $Value -and [string]$Field.TypeAsString -in @("TaxonomyFieldType", "TaxonomyFieldTypeMulti")) {
$valueTypeName = [string]$Value.GetType().FullName
if ($valueTypeName -notlike "*TaxonomyFieldValue*") {
try {
$Value = ([Microsoft.SharePoint.Taxonomy.TaxonomyField]$Field).GetFieldValue([string]$Value)
}
catch {
# Existing export packages can still contain SharePoint's
# serialized WssId;#Label|TermGuid representation. The import
# parser handles that representation as a compatibility path.
}
}
}
return Convert-SPFieldValue $Value
}
function Get-FieldTextValue {
param(
[Parameter(Mandatory = $true)]
@@ -553,7 +585,7 @@ function Get-ListItemFieldMetadata {
InternalName = $field.InternalName
TypeAsString = $field.TypeAsString
TextValue = Get-FieldTextValue -Field $field -RawValue $rawValue
Value = Convert-SPFieldValue $rawValue
Value = Convert-SPItemFieldValueForExport -Field $field -Value $rawValue
}
}
@@ -589,7 +621,7 @@ function Get-ListItemFieldValueMap {
$fieldValues[$field.InternalName] = Get-FieldTextValue -Field $field -RawValue $rawValue
}
else {
$fieldValues[$field.InternalName] = Convert-SPFieldValue $rawValue
$fieldValues[$field.InternalName] = Convert-SPItemFieldValueForExport -Field $field -Value $rawValue
}
}
@@ -1949,127 +1981,6 @@ function Resolve-SPField {
return $null
}
function Resolve-SPAvailableContentTypeForImport {
param(
[Parameter(Mandatory = $true)]
[Microsoft.SharePoint.SPWeb]$Web,
[string]$SourceContentTypeId,
[string]$SourceContentTypeName
)
$sourceId = $null
if (-not [string]::IsNullOrWhiteSpace($SourceContentTypeId)) {
try {
$sourceId = New-Object Microsoft.SharePoint.SPContentTypeId($SourceContentTypeId)
}
catch {
throw ("Ungueltige Source-ContentTypeId: {0}" -f $SourceContentTypeId)
}
}
$availableContentTypes = @($Web.AvailableContentTypes)
if ($null -ne $sourceId) {
foreach ($contentType in $availableContentTypes) {
if ($contentType.Id.Equals($sourceId)) {
return $contentType
}
}
if (-not [string]::IsNullOrWhiteSpace($SourceContentTypeName)) {
$namedParentMatches = @()
foreach ($contentType in $availableContentTypes) {
if ($contentType.Id.IsParentOf($sourceId) -and [string]$contentType.Name -ieq $SourceContentTypeName) {
$namedParentMatches += $contentType
}
}
if ($namedParentMatches.Count -gt 0) {
return @($namedParentMatches | Sort-Object { $_.Id.ToString().Length } -Descending)[0]
}
}
}
if (-not [string]::IsNullOrWhiteSpace($SourceContentTypeName)) {
foreach ($contentType in $availableContentTypes) {
if ([string]$contentType.Name -ieq $SourceContentTypeName) {
return $contentType
}
}
}
return $null
}
function Ensure-SPListContentTypeForImport {
param(
[Parameter(Mandatory = $true)]
[Microsoft.SharePoint.SPList]$List,
[string]$SourceContentTypeId,
[string]$SourceContentTypeName
)
$siteContentType = Resolve-SPAvailableContentTypeForImport -Web $List.ParentWeb -SourceContentTypeId $SourceContentTypeId -SourceContentTypeName $SourceContentTypeName
if ($null -eq $siteContentType) {
throw ("Kein passender publizierter Site-Content-Type gefunden. SourceContentTypeId='{0}', SourceContentTypeName='{1}'" -f $SourceContentTypeId, $SourceContentTypeName)
}
$matchingListContentTypes = @()
foreach ($listContentType in @($List.ContentTypes)) {
if ($listContentType.Id.Equals($siteContentType.Id)) {
return $listContentType
}
if ($siteContentType.Id.IsParentOf($listContentType.Id) -and [string]$listContentType.Name -ieq [string]$siteContentType.Name) {
$matchingListContentTypes += $listContentType
}
}
if ($matchingListContentTypes.Count -gt 0) {
return @($matchingListContentTypes | Sort-Object { $_.Id.ToString().Length })[0]
}
if (-not $List.ContentTypesEnabled) {
$List.ContentTypesEnabled = $true
$List.Update()
}
if (-not $List.IsContentTypeAllowed($siteContentType)) {
throw ("Content-Type '{0}' ({1}) ist fuer die Zielliste '{2}' nicht zulaessig." -f $siteContentType.Name, $siteContentType.Id, $List.Title)
}
$listContentType = $List.ContentTypes.Add($siteContentType)
$List.Update()
Write-Host ("Content-Type zur Zielliste hinzugefuegt: Liste='{0}'; ContentType='{1}'; SiteContentTypeId='{2}'; ListContentTypeId='{3}'" -f $List.Title, $siteContentType.Name, $siteContentType.Id, $listContentType.Id)
return $listContentType
}
function Set-SPItemContentTypeForImport {
param(
[Parameter(Mandatory = $true)]
[Microsoft.SharePoint.SPListItem]$Item,
[Parameter(Mandatory = $true)]
[Microsoft.SharePoint.SPList]$List,
[string]$SourceContentTypeId,
[string]$SourceContentTypeName
)
if ([string]::IsNullOrWhiteSpace($SourceContentTypeId) -and [string]::IsNullOrWhiteSpace($SourceContentTypeName)) {
return
}
$targetContentType = Ensure-SPListContentTypeForImport -List $List -SourceContentTypeId $SourceContentTypeId -SourceContentTypeName $SourceContentTypeName
$Item["ContentTypeId"] = $targetContentType.Id
Write-Host ("Content-Type am Element gesetzt: Liste='{0}'; ContentType='{1}'; ContentTypeId='{2}'" -f $List.Title, $targetContentType.Name, $targetContentType.Id)
}
function Get-FieldMapping {
param(
[Parameter(Mandatory = $true)]
@@ -2322,13 +2233,28 @@ function Convert-ToTaxonomyTermEntries {
if ([string]::IsNullOrWhiteSpace($termGuid) -and ($rawTerm -is [string])) {
$rawTermString = [string]$rawTerm
if ($rawTermString -match "^(.*)\|([0-9a-fA-F-]{36})$") {
$label = $matches[1]
$termGuid = $matches[2]
}
elseif ($rawTermString -match "^[0-9a-fA-F-]{36}$") {
if ($rawTermString -match "^[0-9a-fA-F-]{36}$") {
$termGuid = $rawTermString
}
else {
# SharePoint can expose taxonomy values as either
# Label|TermGuid or WssId;#Label|TermGuid. Multi-value strings
# repeat those pairs with ;# delimiters.
$serializedSegments = @($rawTermString -split ";#|;")
foreach ($serializedSegment in $serializedSegments) {
$candidate = [string]$serializedSegment
if ($candidate -match "^\s*(.*?)\|([0-9a-fA-F-]{36})\s*$") {
$termEntries += [PSCustomObject]@{
Label = $matches[1].Trim()
TermGuid = ([Guid]$matches[2]).ToString()
}
}
}
if ($termEntries.Count -gt 0) {
continue
}
}
}
if ([string]::IsNullOrWhiteSpace($label) -and -not [string]::IsNullOrWhiteSpace($TextValue) -and $rawTerms.Length -le 1) {
@@ -2353,7 +2279,7 @@ function Convert-ToTaxonomyTermEntries {
}
if ($termEntries.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($TextValue)) {
$segments = @($TextValue -split ";#")
$segments = @($TextValue -split ";#|;")
foreach ($segment in $segments) {
if ([string]::IsNullOrWhiteSpace([string]$segment)) {
@@ -2369,7 +2295,7 @@ function Convert-ToTaxonomyTermEntries {
}
}
return @($termEntries)
return @($termEntries | Group-Object -Property TermGuid | ForEach-Object { $_.Group[0] })
}
function New-TaxonomyFieldValueObject {
@@ -2455,6 +2381,16 @@ function Set-SPTaxonomyFieldValue {
if ($termEntries.Count -eq 0) {
if ((Test-SPValuePresent -Value $RawValue) -or -not [string]::IsNullOrWhiteSpace($TextValue)) {
try {
$currentTextValue = [string]$taxonomyField.GetFieldValueAsText($Item[$Field.InternalName])
if (-not [string]::IsNullOrWhiteSpace($currentTextValue) -and $currentTextValue.Trim() -ieq $TextValue.Trim()) {
Write-Host ("Taxonomy-Zielwert ist bereits gesetzt: Feld='{0}'" -f $Field.InternalName)
return
}
}
catch {
}
throw ("Taxonomy-Wert fuer Feld '{0}' enthaelt keine gueltige TermGuid. Der Wert kann nicht in die Ziel-SiteCollection aufgeloest werden." -f $Field.InternalName)
}
@@ -2693,6 +2629,11 @@ function Apply-FieldMappingToItem {
$null
}
if (-not (Test-SPValuePresent -Value $rawValue) -and -not (Test-SPValuePresent -Value $textValue)) {
Write-Host ("Leerer Columnwert uebersprungen: {0}; SourceColumn='{1}'; TargetColumn='{2}'" -f $Context, $sourceInternalName, $targetInternalName)
continue
}
try {
$fieldWasSet = Set-SPItemFieldValue -Item $Item -TargetInternalName $targetInternalName -RawValue $rawValue -TextValue $textValue
if ($fieldWasSet) {
@@ -2745,8 +2686,49 @@ function Test-SPValuePresent {
return -not [string]::IsNullOrWhiteSpace($Value)
}
if ((Test-ObjectHasProperty -Object $Value -PropertyName "TermGuid") -or (Test-ObjectHasProperty -Object $Value -PropertyName "Label")) {
$termGuid = Get-ObjectPropertyValue -Object $Value -PropertyName "TermGuid"
$label = Get-ObjectPropertyValue -Object $Value -PropertyName "Label"
return (Test-SPValuePresent -Value $termGuid) -or (Test-SPValuePresent -Value $label)
}
if ($Value -is [System.Collections.IDictionary]) {
if ($Value.Count -eq 0) {
return $false
}
foreach ($dictionaryValue in $Value.Values) {
if (Test-SPValuePresent -Value $dictionaryValue) {
return $true
}
}
return $false
}
if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
return @($Value).Count -gt 0
foreach ($entry in $Value) {
if (Test-SPValuePresent -Value $entry) {
return $true
}
}
return $false
}
if ($Value -is [System.Management.Automation.PSCustomObject]) {
$properties = @($Value.PSObject.Properties)
if ($properties.Count -eq 0) {
return $false
}
foreach ($property in $properties) {
if (Test-SPValuePresent -Value $property.Value) {
return $true
}
}
return $false
}
return $true
@@ -3127,15 +3109,6 @@ function Import-SPDocumentLibraries {
$importStage = "Metadaten setzen"
$fieldErrors = New-Object 'System.Collections.Generic.List[string]'
try {
Set-SPItemContentTypeForImport -Item $spItem -List $targetLibrary -SourceContentTypeId ([string](Get-ObjectPropertyValue -Object $itemMetadata -PropertyName "ContentTypeId")) -SourceContentTypeName ([string](Get-ObjectPropertyValue -Object $itemMetadata -PropertyName "ContentTypeName"))
}
catch {
$contentTypeError = "Content-Type konnte fuer Bibliothek='{0}'; Datei='{1}' nicht gesetzt werden. {2}" -f $targetLibrary.Title, $fileName, $_.Exception.Message
Write-Warning $contentTypeError
[void]$fieldErrors.Add($contentTypeError)
}
$fieldMapping = Get-FieldMappingForContainer -MigrationMappingTable $MigrationMappingTable -ObjectType "DocumentLibrary" -SourceTitle $sourceLibraryTitle
Write-Host ("Gefundene Metadaten-Mappings: Bibliothek='{0}'; Anzahl={1}" -f $targetLibrary.Title, $fieldMapping.Count)
Apply-FieldMappingToItem -Item $spItem -FieldMapping $fieldMapping -SourceFieldValues (Get-ObjectPropertyValue -Object $itemMetadata -PropertyName "FieldValues") -SourceFieldTextValues (Get-ObjectPropertyValue -Object $itemMetadata -PropertyName "FieldTextValues") -Context ("Bibliothek='{0}'; Datei='{1}'" -f $targetLibrary.Title, $fileName) -Errors $fieldErrors
@@ -3297,13 +3270,6 @@ function Import-SPLists {
$targetItem = $targetList.Items.Add()
}
try {
Set-SPItemContentTypeForImport -Item $targetItem -List $targetList -SourceContentTypeId ([string](Get-ObjectPropertyValue -Object $sourceItem -PropertyName "ContentTypeId")) -SourceContentTypeName ([string](Get-ObjectPropertyValue -Object $sourceItem -PropertyName "ContentTypeName"))
}
catch {
Write-Warning ("Content-Type fuer Listenelement konnte nicht gesetzt werden; der Standard-Content-Type wird verwendet. Liste='{0}'; SourceItemId='{1}'; Fehler={2}" -f $targetListTitle, $sourceItemId, $_.Exception.Message)
}
Apply-FieldMappingToItem -Item $targetItem -FieldMapping $fieldMapping -SourceFieldValues (Get-ObjectPropertyValue -Object $sourceItem -PropertyName "FieldValues") -SourceFieldTextValues (Get-ObjectPropertyValue -Object $sourceItem -PropertyName "FieldTextValues") -Context ("Liste='{0}'; SourceItemId='{1}'; Titel='{2}'" -f $targetListTitle, $sourceItemId, $sourceItemTitle)
if (-not [string]::IsNullOrWhiteSpace($sourceItemUniqueId)) {