Private/Validation.ps1
|
<# Règles de validation pré-vol. C'est la fonctionnalité la plus rentable de l'outil : elle transforme des échecs en cours de migration (coûteux, partiels, difficiles à reprendre) en anomalies connues AVANT le lancement. ShareGate et SPMT refusent de démarrer sans cette étape ; la version précédente de cet outil ne la faisait pas du tout. Les fonctions ici sont volontairement pures (aucun appel réseau) : elles sont testables unitairement et réutilisables pour valider un plan hors connexion. #> # Caractères interdits dans un nom de fichier ou de dossier SharePoint Online. $script:SPMInvalidNameChars = @('"', '*', ':', '<', '>', '?', '/', '\', '|') # Noms réservés (héritage Windows) refusés par SharePoint. $script:SPMReservedNames = @( 'CON', 'PRN', 'AUX', 'NUL', 'COM0', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT0', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9', '_vti_', 'desktop.ini' ) # Limites SharePoint Online (documentées par Microsoft, valeurs 2026). $script:SPMLimits = @{ MaxServerRelativeUrlLength = 400 # URL complète décodée MaxSegmentLength = 255 # un segment de chemin MaxFileSizeBytes = 250GB ListViewThreshold = 5000 } function Test-SPMItemName { <# .SYNOPSIS Valide le nom d'un fichier ou d'un dossier au regard des règles SharePoint. .OUTPUTS Liste de chaînes décrivant les problèmes. Vide = nom valide. #> [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory)][AllowEmptyString()][string]$Name, [switch]$IsFolder ) $problems = [System.Collections.Generic.List[string]]::new() if ([string]::IsNullOrWhiteSpace($Name)) { $problems.Add('Nom vide') return $problems.ToArray() } $found = $script:SPMInvalidNameChars | Where-Object { $Name.Contains($_) } if ($found) { $problems.Add("Caractères interdits : $($found -join ' ')") } if ($Name.StartsWith('.') -or $Name.EndsWith('.')) { $problems.Add("Ne peut ni commencer ni finir par un point") } if ($Name -ne $Name.Trim()) { $problems.Add('Espaces en début ou fin de nom') } if ($Name.Contains('..')) { $problems.Add("Séquence '..' interdite") } if ($Name.Length -gt $script:SPMLimits.MaxSegmentLength) { $problems.Add("Segment trop long ($($Name.Length) > $($script:SPMLimits.MaxSegmentLength) caractères)") } $bare = [System.IO.Path]::GetFileNameWithoutExtension($Name) if ($script:SPMReservedNames -contains $bare.ToUpperInvariant() -or $script:SPMReservedNames -contains $Name.ToUpperInvariant()) { $problems.Add("Nom réservé : $Name") } if ($Name.StartsWith('~$')) { $problems.Add("Fichier temporaire Office (~$) : à exclure de la migration") } return $problems.ToArray() } function Test-SPMUrlLength { <# .SYNOPSIS Vérifie qu'une URL serveur-relative reste sous la limite SharePoint. .DESCRIPTION Cause d'échec numéro un des migrations depuis un partage de fichiers : l'arborescence source est profonde et la destination ajoute un préfixe. On valide donc l'URL de destination réellement projetée. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][string]$ServerRelativeUrl, [int]$MaxLength = 0 ) if ($MaxLength -le 0) { $MaxLength = $script:SPMLimits.MaxServerRelativeUrlLength } $decoded = [System.Uri]::UnescapeDataString($ServerRelativeUrl) $tooLongSegments = @($decoded.Split('/') | Where-Object { $_.Length -gt $script:SPMLimits.MaxSegmentLength }) [pscustomobject]@{ Url = $ServerRelativeUrl Length = $decoded.Length MaxLength = $MaxLength Ok = ($decoded.Length -le $MaxLength -and $tooLongSegments.Count -eq 0) Margin = $MaxLength - $decoded.Length LongSegments = $tooLongSegments RecommendedFix = if ($decoded.Length -gt $MaxLength) { "Raccourcir de $($decoded.Length - $MaxLength) caractères, ou migrer vers une bibliothèque au chemin plus court" } else { $null } } } function Test-SPMFileConstraint { <# .SYNOPSIS Valide un fichier candidat (taille, extension, état d'extraction). #> [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory)][string]$Name, [long]$SizeBytes = -1, [string[]]$BlockedExtensions = @(), [bool]$IsCheckedOut = $false, [switch]$AllowEmptyFile ) $problems = [System.Collections.Generic.List[string]]::new() $nameProblems = @(Test-SPMItemName -Name $Name) if ($nameProblems.Count -gt 0) { $problems.AddRange([string[]]$nameProblems) } if ($SizeBytes -ge 0) { if ($SizeBytes -gt $script:SPMLimits.MaxFileSizeBytes) { $problems.Add("Fichier de $([math]::Round($SizeBytes / 1GB, 2)) Go : au-delà de la limite SharePoint de 250 Go") } if ($SizeBytes -eq 0 -and -not $AllowEmptyFile) { $problems.Add('Fichier vide (0 octet) : rejeté par la Migration API') } } if ($BlockedExtensions.Count -gt 0) { $ext = [System.IO.Path]::GetExtension($Name) if ($ext -and ($BlockedExtensions -contains $ext.ToLowerInvariant())) { $problems.Add("Extension bloquée par la configuration : $ext") } } if ($IsCheckedOut) { $problems.Add('Fichier extrait (checked out) : le contenu non archivé ne sera pas migré') } return $problems.ToArray() } function New-SPMValidationFinding { <# .SYNOPSIS Crée un constat de validation normalisé. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][ValidateSet('Error', 'Warning', 'Info')][string]$Severity, [Parameter(Mandatory)][string]$Rule, [Parameter(Mandatory)][string]$Message, [string]$Path, [string]$Remediation ) [pscustomobject]@{ Severity = $Severity Rule = $Rule Message = $Message Path = $Path Remediation = $Remediation } } |