Update application version to 1.3.4 and enhance support for SharePoint Subscription Edition in migration scripts and documentation
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Start-SPMigration
|
||||
|
||||
Version 1.2.0
|
||||
Version 1.3.4
|
||||
|
||||
PowerShell-Werkzeug fuer die Migration von SharePoint-On-Premises-Inhalten ueber das serverseitige Objektmodell (SSOM). Unterstuetzt werden SharePoint Server 2016 und SharePoint Server 2019.
|
||||
PowerShell-Werkzeug fuer die Migration von SharePoint-On-Premises-Inhalten ueber das serverseitige Objektmodell (SSOM). Unterstuetzt werden SharePoint Server 2016, SharePoint Server 2019 und SharePoint Server Subscription Edition (SE).
|
||||
|
||||
Das Skript kann:
|
||||
|
||||
@@ -17,11 +17,11 @@ Das Skript kann:
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- SharePoint Server 2016 oder SharePoint Server 2019
|
||||
- SharePoint Server 2016, SharePoint Server 2019 oder SharePoint Server Subscription Edition
|
||||
- lokale Ausfuehrung auf einem SharePoint-Farmserver; SSOM kann keine entfernte Farm adressieren
|
||||
- 64-Bit Windows PowerShell (Desktop Edition), nicht PowerShell 7
|
||||
- Hauptskript: Windows PowerShell 4.0 oder neuer
|
||||
- GUI: Windows PowerShell 5.0 oder neuer im STA-Modus; Windows PowerShell 5.1 wird empfohlen
|
||||
- GUI: Windows PowerShell 5.0 oder neuer im STA-Modus; Windows PowerShell 5.1 und Windows Server mit Desktop Experience werden empfohlen
|
||||
- Farm- und Web-Berechtigungen fuer die ausgefuehrten Export-/Importaktionen
|
||||
|
||||
Das Werkzeug versucht zuerst das SharePoint-Modul und anschliessend den auf klassischen Farmen ueblichen Snap-in-Fallback zu laden:
|
||||
@@ -31,7 +31,7 @@ Import-Module SharePointServer
|
||||
Add-PSSnapin Microsoft.SharePoint.PowerShell
|
||||
```
|
||||
|
||||
Die lokale Farmversion wird anhand von `SPFarm.Local.BuildVersion` erkannt. Andere Versionen, einschliesslich Subscription Edition, werden bewusst abgelehnt. Die Umgebung kann vor einer Migration separat geprueft werden:
|
||||
Die lokale Farmversion wird anhand von `SPFarm.Local.BuildVersion` erkannt. Die Buildbereiche von 2016, 2019 und Subscription Edition werden akzeptiert; andere SharePoint-Versionen werden abgelehnt. Die Umgebung kann vor einer Migration separat geprueft werden:
|
||||
|
||||
```powershell
|
||||
.\Start-SPMigration.ps1 -ValidateEnvironment
|
||||
@@ -39,13 +39,13 @@ Die lokale Farmversion wird anhand von `SPFarm.Local.BuildVersion` erkannt. Ande
|
||||
|
||||
### Migration zwischen zwei Farmen
|
||||
|
||||
Eine Migration von SharePoint 2016 nach SharePoint 2019 erfolgt mit SSOM zwingend zweistufig:
|
||||
Eine Migration zwischen 2016, 2019 und Subscription Edition erfolgt mit SSOM zwingend zweistufig:
|
||||
|
||||
1. Export lokal auf einem Server der SharePoint-2016-Quellfarm ausfuehren.
|
||||
2. Den kompletten Exportordner auf einen Server der SharePoint-2019-Zielfarm kopieren.
|
||||
1. Export lokal auf einem Server der Quellfarm ausfuehren.
|
||||
2. Den kompletten Exportordner auf einen Server der Zielfarm kopieren.
|
||||
3. Import dort lokal gegen das Ziel-Web ausfuehren.
|
||||
|
||||
Ein kombinierter Export und Import in demselben Prozess ist nur sinnvoll, wenn Quelle und Ziel in derselben lokalen Farm liegen. Ein Import von 2019 nach 2016 ist ein nicht garantierter Downgrade und erzeugt eine Warnung.
|
||||
Vorgesehene Upgrade-Pfade sind insbesondere 2016 nach 2019, 2016 nach SE und 2019 nach SE. Ein kombinierter Export und Import in demselben Prozess ist nur sinnvoll, wenn Quelle und Ziel in derselben lokalen Farm liegen. Downgrades von SE nach 2019/2016 oder von 2019 nach 2016 erzeugen eine Warnung, da neuere Artefakte im Ziel fehlen koennen.
|
||||
|
||||
## Dateien im Projekt
|
||||
|
||||
@@ -82,7 +82,7 @@ OutputPath
|
||||
|
||||
- Pro Liste wird genau eine JSON-Datei unter `Lists\` erzeugt.
|
||||
- Die Eintraege enthalten insbesondere:
|
||||
- `ContentTypeId`
|
||||
- `ContentTypeId` (nur Exportinformation)
|
||||
- `FieldValues`
|
||||
- `FieldTextValues`
|
||||
- `Fields`
|
||||
@@ -93,10 +93,9 @@ Fuer den Import sind vor allem diese Informationen relevant:
|
||||
|
||||
- `FieldValues`
|
||||
- `FieldTextValues`
|
||||
- `ContentTypeId`
|
||||
- interne Feldnamen (`InternalName`)
|
||||
|
||||
Der Inhaltstyp wird zuerst ueber eine exakt passende ID beziehungsweise einen gleichnamigen Site-Content-Type aufgeloest. Ein beliebiger Basis-Content-Type wie `Item` oder `Document` wird nicht mehr still als Ersatz fuer einen fehlenden eigenen Content-Type verwendet.
|
||||
`ContentTypeId` und `ContentTypeName` der Quelle werden nur als Exportinformation gespeichert und beim Import vollstaendig ignoriert. Der Import behaelt den vom Zielcontainer vorgegebenen Content Type und fuehrt ausschliesslich das Columnmapping von `SourceInternalName` auf `TargetInternalName` aus. Dadurch duerfen sich Content Types und Spaltennamen zwischen Quelle und Ziel unterscheiden.
|
||||
|
||||
## MappingTable.json
|
||||
|
||||
@@ -112,7 +111,7 @@ Beispiel:
|
||||
```json
|
||||
{
|
||||
"SchemaVersion": 2,
|
||||
"ToolVersion": "1.2.0",
|
||||
"ToolVersion": "1.3.4",
|
||||
"GeneratedAtUtc": "2026-04-15T08:00:00.0000000Z",
|
||||
"SourceWebUrl": "http://sharepoint/sites/Quelle",
|
||||
"SourceSharePointProduct": "SharePoint Server 2016",
|
||||
@@ -190,7 +189,9 @@ Fuer das Mapping ist vor allem relevant:
|
||||
- `CustomColumns`: eigene Fachspalten, die im Ziel oft umbenannt oder anders aufgebaut sind
|
||||
- `SourceSupportingInternalNames`: technische Begleitspalten wie `_0` werden nur noch informativ aufgefuehrt und nicht separat gemappt
|
||||
- `ImportSupported`: steuert, ob eine Spalte beim Import ueberhaupt beruecksichtigt wird
|
||||
- Sind Rohwert und Textwert einer Quellspalte leer, wird das Mapping fuer dieses Element uebersprungen. Das Zielfeld wird dann weder geleert noch anderweitig veraendert.
|
||||
- Taxonomy-Felder (Einfach- und Mehrfachwerte) werden beim Import ueber den exportierten `TermGuid` gesetzt; dabei wird angenommen, dass dieselben Terme im Ziel bereits mit identischer GUID existieren
|
||||
- Taxonomy-Werte werden sowohl als strukturiertes `Label + TermGuid` als auch im SharePoint-Format `WssId;#Label|TermGuid` gelesen. Neue Exporte normalisieren den Rohwert bereits beim Schreiben des Sidecars.
|
||||
- Die exportierte `WssId` wird bewusst nicht uebernommen: Sie ist nur innerhalb der Quell-SiteCollection gueltig. SharePoint loest beim Setzen des Feldwerts die zielseitige `WssId` auf und legt den Eintrag in der `TaxonomyHiddenList` bei Bedarf selbst an. Die Hidden List soll deshalb nicht separat migriert oder vorab befuellt werden.
|
||||
- Klassische `Lookup`- und `LookupMulti`-Spalten werden derzeit explizit als nicht importierbar markiert. Quellseitige Lookup-IDs sind in einer anderen Farm nicht portabel; ein korrekter Ausbau benoetigt ein fachliches Schluessel-Mapping und einen zweiphasigen Import.
|
||||
|
||||
@@ -243,7 +244,7 @@ Beispiel:
|
||||
|
||||
### Umgebungspruefung
|
||||
|
||||
- `ValidateEnvironment`: prueft Windows PowerShell, x64-Prozess, SharePoint-Cmdlets, Taxonomy-Assembly und die lokale SharePoint-2016/2019-Farm, ohne eine Migration zu starten
|
||||
- `ValidateEnvironment`: prueft Windows PowerShell, x64-Prozess, SharePoint-Cmdlets, Taxonomy-Assembly und die lokale SharePoint-2016/2019/SE-Farm, ohne eine Migration zu starten
|
||||
|
||||
### Exportparameter
|
||||
|
||||
@@ -298,7 +299,7 @@ Hinweis:
|
||||
-MappingTable "C:\Temp\SP-Export\MappingTable.json"
|
||||
```
|
||||
|
||||
### SharePoint 2016 exportieren, MappingTable anpassen und auf SharePoint 2019 importieren
|
||||
### Farm exportieren, MappingTable anpassen und auf 2016, 2019 oder SE importieren
|
||||
|
||||
```powershell
|
||||
.\Start-SPMigration.ps1 `
|
||||
@@ -307,7 +308,7 @@ Hinweis:
|
||||
-Export
|
||||
```
|
||||
|
||||
Danach den kompletten Ordner `C:\Temp\SP-Export` auf den SharePoint-2019-Farmserver kopieren und `MappingTable.json` bearbeiten. Dort insbesondere `TargetTitle` sowie `TargetInternalName` pruefen.
|
||||
Danach den kompletten Ordner `C:\Temp\SP-Export` auf den Farmserver des Ziels kopieren und `MappingTable.json` bearbeiten. Dort insbesondere `TargetTitle` sowie `TargetInternalName` pruefen.
|
||||
|
||||
Anschliessend:
|
||||
|
||||
@@ -366,12 +367,13 @@ Anschliessend:
|
||||
- `Lookup`- und `LookupMulti`-Spalten werden nicht importiert, weil Quell-IDs auf der Zielfarm keine stabile Bedeutung haben.
|
||||
- SharePoint-Gruppen in Personenfeldern werden exportseitig erkannt, aber noch nicht automatisch einer Zielgruppe zugeordnet; ein nicht aufloesbarer nichtleerer Wert wird als Feldfehler gemeldet.
|
||||
- Fehlende Ziellisten oder Zielbibliotheken werden derzeit nicht automatisch angelegt; das Skript gibt stattdessen eine Warnung zur manuellen Anlage aus.
|
||||
- Quell-Content-Types werden beim Import nicht gemappt oder gesetzt; der Zielcontainer bestimmt den Content Type.
|
||||
- Bereits vorhandene Dateien unter `Files` und `Lists` werden vor einem erneuten Export nicht automatisch entfernt. Fuer jeden produktiven Lauf sollte deshalb ein neuer, leerer `OutputPath` verwendet werden.
|
||||
- Schlaegt das Setzen von Metadaten nach dem Datei-Upload fehl, kann die bereits hochgeladene Datei als Teilimport im Ziel verbleiben. Der Fehler wird protokolliert; eine automatische Rollback-/Quarantaene-Logik ist noch nicht vorhanden.
|
||||
- Dateiversionen, Check-in/Publish/Approve-Zustaende und Listen-Versionen werden noch nicht vollstaendig rekonstruiert.
|
||||
- Grosse Listen werden derzeit als eine JSON-Datei verarbeitet; fuer sehr grosse Datenmengen fehlt noch ein paginierter Chunk-Export.
|
||||
- Sehr spezielle Feldtypen koennen je nach Farm-Konfiguration zusaetzliche Anpassungen benoetigen.
|
||||
- Das Skript ist fuer SharePoint Server 2016/2019 mit SSOM gedacht, nicht fuer SharePoint Online, CSOM, PnP PowerShell oder Subscription Edition.
|
||||
- Das Skript ist fuer SharePoint Server 2016, 2019 und Subscription Edition mit SSOM gedacht, nicht fuer SharePoint Online, CSOM oder PnP PowerShell.
|
||||
- SharePoint Server 2016 und 2019 haben das Ende des erweiterten Microsoft-Supports erreicht. Das Werkzeug unterstuetzt sie technisch weiter, ersetzt aber keine Plattform-Sicherheitsstrategie.
|
||||
|
||||
## Empfohlene naechste Ausbaustufen
|
||||
|
||||
+117
-151
@@ -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)) {
|
||||
|
||||
@@ -8,7 +8,7 @@ param(
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$script:ApplicationVersion = "1.2.0"
|
||||
$script:ApplicationVersion = "1.3.4"
|
||||
$script:SupportedMappingSchemaVersion = 2
|
||||
|
||||
$editionProperty = $PSVersionTable.PSObject.Properties["PSEdition"]
|
||||
@@ -884,6 +884,7 @@ function Get-SharePointProductSupportInfo {
|
||||
}
|
||||
elseif ($BuildVersion.Major -eq 16 -and $BuildVersion.Build -ge 14326) {
|
||||
$productName = "SharePoint Server Subscription Edition"
|
||||
$isSupported = $true
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
@@ -947,7 +948,7 @@ function Initialize-SharePointPowerShellForGui {
|
||||
$buildVersion = [version]$farm.BuildVersion
|
||||
$supportInfo = Get-SharePointProductSupportInfo -BuildVersion $buildVersion
|
||||
if (-not $supportInfo.IsSupported) {
|
||||
throw ("Nicht unterstuetzte lokale SharePoint-Farm: {0}, Build {1}. Unterstuetzt werden SharePoint Server 2016 und 2019." -f $supportInfo.ProductName, $buildVersion)
|
||||
throw ("Nicht unterstuetzte lokale SharePoint-Farm: {0}, Build {1}. Unterstuetzt werden SharePoint Server 2016, 2019 und Subscription Edition." -f $supportInfo.ProductName, $buildVersion)
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
@@ -3174,7 +3175,7 @@ $script:MainForm.Add_Shown({
|
||||
$btnLoadContainers.Enabled = $false
|
||||
$btnRunExport.Enabled = $false
|
||||
$btnRunImport.Enabled = $false
|
||||
Show-UiMessage -Message ("Die Migration wurde deaktiviert, weil keine unterstuetzte lokale SharePoint-2016/2019-SSOM-Umgebung erkannt wurde. Fehler: {0}" -f $_.Exception.Message) -Caption "SSOM-Umgebung nicht verfuegbar" -Icon ([System.Windows.Forms.MessageBoxIcon]::Error)
|
||||
Show-UiMessage -Message ("Die Migration wurde deaktiviert, weil keine unterstuetzte lokale SharePoint-2016/2019/SE-SSOM-Umgebung erkannt wurde. Fehler: {0}" -f $_.Exception.Message) -Caption "SSOM-Umgebung nicht verfuegbar" -Icon ([System.Windows.Forms.MessageBoxIcon]::Error)
|
||||
}
|
||||
Load-UiSettings
|
||||
Set-MappingTableToUi -MappingTable (New-EmptyMappingTable)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"SchemaVersion": 2,
|
||||
"ToolVersion": "1.2.0",
|
||||
"ToolVersion": "1.3.4",
|
||||
"GeneratedAtUtc": "2026-04-15T08:00:00.0000000Z",
|
||||
"SourceWebUrl": "http://sharepoint/sites/Quelle",
|
||||
"SourceSharePointProduct": "SharePoint Server 2016",
|
||||
|
||||
Reference in New Issue
Block a user