Public/Test-sqmRobocopyParameter.ps1
|
function Test-sqmRobocopyParameter { <# .SYNOPSIS Prueft robocopy-Parameter offline auf Fehler und riskante Kombinationen. .DESCRIPTION Liefert je Befund ein Objekt mit Severity (Error | Warning), Parameter und Message. Fehler fuehren bei robocopy zu "Ungueltiger Parameter" (Exitcode 16) oder sind widerspruechlich; Warnungen markieren Optionen, die Daten loeschen, ewig laufen oder Adminrechte brauchen. Mit -CheckPath werden Quelle und Ziel zusaetzlich im Dateisystem geprueft. .EXAMPLE Test-sqmRobocopyParameter -Parameter @{ Source = 'D:\A'; Destination = 'D:\A\Kopie'; MIR = $true } #> [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Parameter, [switch]$CheckPath ) $issues = New-Object System.Collections.Generic.List[object] $add = { param($sev, $par, $msg) $issues.Add([pscustomobject]@{ Severity = $sev; Parameter = $par; Message = $msg }) } $lookup = Get-srgParameterLookup foreach ($k in $Parameter.Keys) { if ($k -notin 'Source', 'Destination', 'Files' -and -not $lookup.ContainsKey($k)) { & $add 'Error' $k "Unbekannte robocopy-Option /$k." } } $o = Get-srgActiveParameter $Parameter $has = { param($n) $o.Contains($n) } # --- Quelle / Ziel ---------------------------------------------------------------------------- $src = ([string]$Parameter['Source']).Trim().Trim('"') $dst = ([string]$Parameter['Destination']).Trim().Trim('"') if (-not $src -and -not (& $has 'NOSD')) { & $add 'Error' 'Source' 'Quelle fehlt.' } if (-not $dst -and -not (& $has 'NODD')) { & $add 'Error' 'Destination' 'Ziel fehlt.' } foreach ($pair in @(@('Source', $src), @('Destination', $dst))) { if ($pair[1] -and $pair[1] -match '[*?<>|]') { & $add 'Error' $pair[0] "Verzeichnis darf keine Platzhalter enthalten - Dateimuster gehoeren in 'Dateien'." } elseif ($pair[1] -and $pair[1] -notmatch '^([A-Za-z]:|\\\\[^\\]+\\[^\\]+)' -and $pair[1] -notmatch '^\.') { & $add 'Warning' $pair[0] 'Relativer Pfad - er wird zum aktuellen Verzeichnis von robocopy aufgeloest. Besser absolut (D:\... oder \\Server\Freigabe\...).' } } if ($src -and $dst) { $s = (ConvertTo-srgDirectory $src).TrimEnd('\').ToLowerInvariant() $d = (ConvertTo-srgDirectory $dst).TrimEnd('\').ToLowerInvariant() $recursive = (& $has 'S') -or (& $has 'E') -or (& $has 'MIR') -or (& $has 'MOVE') if ($s -eq $d) { & $add 'Error' 'Destination' 'Quelle und Ziel sind identisch.' } elseif ($recursive -and $d.StartsWith($s + '\')) { & $add 'Warning' 'Destination' 'Das Ziel liegt innerhalb der Quelle - mit Unterverzeichnissen kopiert robocopy dann in sich selbst hinein. Ziel ausserhalb waehlen oder per /XD ausschliessen.' } elseif (((& $has 'MIR') -or (& $has 'PURGE')) -and $s.StartsWith($d + '\')) { & $add 'Error' 'Destination' 'Die Quelle liegt innerhalb des Ziels - /MIR bzw. /PURGE wuerde im Ziel alles ausser der Quelle loeschen.' } } if ($CheckPath) { if ($src -and -not (& $has 'NOSD') -and -not (Test-Path -LiteralPath $src -PathType Container -ErrorAction SilentlyContinue)) { & $add 'Error' 'Source' "Quelle '$src' ist nicht erreichbar oder kein Verzeichnis." } if ($dst -and -not (& $has 'NODD')) { $root = [System.IO.Path]::GetPathRoot($dst) if ($root -and -not (Test-Path -LiteralPath $root -ErrorAction SilentlyContinue)) { & $add 'Error' 'Destination' "Ziellaufwerk/-freigabe '$root' ist nicht erreichbar." } } } foreach ($f in (ConvertTo-srgList $Parameter['Files'])) { if ($f -match '[\\/]') { & $add 'Warning' 'Files' "Dateimuster '$f' enthaelt einen Pfad - robocopy erwartet hier nur Namen oder Platzhalter." } } # --- Exklusive Gruppen ----------------------------------------------------------------------- foreach ($grp in ($script:srgParameters | Where-Object { $_.Exclusive } | Group-Object { $_.Exclusive })) { $set = @($grp.Group | Where-Object { & $has $_.Name } | ForEach-Object { '/' + $_.Name }) if ($set.Count -gt 1) { & $add 'Error' ($set[0].TrimStart('/')) "Schliessen sich gegenseitig aus: $($set -join ', ')." } } # --- Werte ------------------------------------------------------------------------------------ foreach ($name in $o.Keys) { $d = $lookup[$name] $v = $o[$name] if ($v -is [bool]) { continue } $kind = if ($d.Type -eq 'Opt') { $d.ValueKind } else { $d.Type } switch ($kind) { 'Int' { $n = 0 if (-not [int]::TryParse($v, [ref]$n)) { & $add 'Error' $name "/$name erwartet eine ganze Zahl."; break } if ($null -ne $d.Min -and ($n -lt $d.Min -or $n -gt $d.Max)) { & $add 'Error' $name "/$name muss zwischen $($d.Min) und $($d.Max) liegen." } } 'Bytes' { if ($v -notmatch '^\d+$') { & $add 'Error' $name "/$name erwartet eine Zahl in Bytes (robocopy kennt hier kein K/M/G - z.B. 1048576 fuer 1 MB)." } } 'Size' { if ($v -notmatch '^\d+[KMG]?$') { & $add 'Error' $name "/$name erwartet n oder n mit K, M oder G (z.B. 50M)." } } 'Enum' { if ($v -notin $d.Values) { & $add 'Error' $name "/$name erlaubt nur $($d.Values -join ' oder ')." } } 'Flags' { $bad = @($v.ToCharArray() | Where-Object { $d.Letters.IndexOf([string]$_) -lt 0 } | Select-Object -Unique) if ($bad.Count) { & $add 'Error' $name "/$name erlaubt nur die Buchstaben $($d.Letters) (ungueltig: $($bad -join ''))." } } 'Age' { if ($v -notmatch '^\d+$') { & $add 'Error' $name "/$name erwartet Tage (< 1900) oder ein Datum JJJJMMTT."; break } if ([long]$v -ge 1900) { $dt = [datetime]::MinValue if (-not [datetime]::TryParseExact($v, 'yyyyMMdd', [cultureinfo]::InvariantCulture, 'None', [ref]$dt)) { & $add 'Error' $name "/${name}:$v ist kein gueltiges Datum JJJJMMTT (Werte ab 1900 gelten als Datum)." } } } 'Hours' { if ($v -notmatch '^(\d{4})-(\d{4})$' -or [int]$Matches[1].Substring(2) -gt 59 -or [int]$Matches[2].Substring(2) -gt 59 -or [int]$Matches[1] -gt 2400 -or [int]$Matches[2] -gt 2400) { & $add 'Error' $name '/RH erwartet hhmm-hhmm (z.B. 2200-0600).' } elseif ($Matches[1] -eq $Matches[2]) { & $add 'Error' $name '/RH: Start und Ende sind gleich.' } } 'Path' { if ($v -match '[*?<>|]') { & $add 'Error' $name "/$name enthaelt unzulaessige Zeichen." } } } } if ((& $has 'MIN') -and (& $has 'MAX') -and $o['MIN'] -match '^\d+$' -and $o['MAX'] -match '^\d+$' -and [long]$o['MIN'] -gt [long]$o['MAX']) { & $add 'Error' 'MIN' '/MIN ist groesser als /MAX - es wuerde keine Datei kopiert.' } # --- Kombinationen --------------------------------------------------------------------------- if (& $has 'MT') { if (& $has 'IPG') { & $add 'Error' 'IPG' '/IPG ist nicht mit /MT kombinierbar (robocopy ignoriert dann eine der Optionen).' } if (& $has 'EFSRAW') { & $add 'Error' 'EFSRAW' '/EFSRAW ist nicht mit /MT kombinierbar.' } } if ((& $has 'NOCOPY') -and -not (& $has 'PURGE') -and -not (& $has 'MIR')) { & $add 'Warning' 'NOCOPY' '/NOCOPY ohne /PURGE bzw. /MIR kopiert nichts.' } if ((& $has 'PURGE') -and (& $has 'MIR')) { & $add 'Warning' 'PURGE' '/PURGE ist in /MIR bereits enthalten.' } if ((& $has 'PURGE') -and -not ((& $has 'E') -or (& $has 'S') -or (& $has 'MIR'))) { & $add 'Warning' 'PURGE' '/PURGE ohne /E loescht nur im obersten Zielverzeichnis.' } if ((& $has 'XN') -and (& $has 'XO')) { & $add 'Warning' 'XO' '/XN und /XO zusammen: es werden nur Dateien kopiert, die im Ziel fehlen.' } if ((& $has 'XX') -and -not ((& $has 'MIR') -or (& $has 'PURGE'))) { & $add 'Warning' 'XX' '/XX wirkt nur zusammen mit /MIR oder /PURGE.' } if ((& $has 'TEE') -and -not ((& $has 'LOG') -or (& $has 'LOG+') -or (& $has 'UNILOG') -or (& $has 'UNILOG+'))) { & $add 'Warning' 'TEE' '/TEE ohne Protokolldatei hat keine Wirkung.' } if ((& $has 'PF') -and -not (& $has 'RH')) { & $add 'Warning' 'PF' '/PF wirkt nur zusammen mit /RH.' } if (((& $has 'Threshold')) -and -not ((& $has 'IoRate') -or (& $has 'IoMaxSize'))) { & $add 'Warning' 'Threshold' '/Threshold wirkt nur zusammen mit /IoRate oder /IoMaxSize.' } if (((& $has 'LOG') -or (& $has 'LOG+') -or (& $has 'UNILOG') -or (& $has 'UNILOG+')) -and -not (& $has 'NP')) { & $add 'Warning' 'NP' 'Protokolldatei ohne /NP: jede Prozentanzeige landet in der Datei. /NP setzen.' } if ((& $has 'SAVE') -and (& $has 'MT')) { & $add 'Warning' 'SAVE' 'robocopy speichert /MT nicht in der Auftragsdatei.' } if ((& $has 'JOB') -and -not (& $has 'NOSD') -and -not $src) { & $add 'Warning' 'NOSD' 'Kommt die Quelle aus der Auftragsdatei, /NOSD setzen.' } # --- Riskantes ------------------------------------------------------------------------------- if (-not (& $has 'L')) { if (& $has 'MIR') { & $add 'Warning' 'MIR' '/MIR loescht im Ziel alles, was in der Quelle fehlt. Erst einen Trockenlauf (/L) machen.' } if (& $has 'PURGE') { & $add 'Warning' 'PURGE' '/PURGE loescht im Ziel alles, was in der Quelle fehlt.' } if (& $has 'MOV') { & $add 'Warning' 'MOV' '/MOV loescht die Dateien nach dem Kopieren in der Quelle.' } if (& $has 'MOVE') { & $add 'Warning' 'MOVE' '/MOVE loescht Dateien und Verzeichnisse nach dem Kopieren in der Quelle.' } if (((& $has 'MIR') -or (& $has 'PURGE')) -and $dst -match '^([A-Za-z]:|\\\\[^\\]+\\[^\\]+)\\?$') { & $add 'Warning' 'Destination' 'Das Ziel ist eine Laufwerks- bzw. Freigabewurzel - /MIR oder /PURGE raeumt dort ALLES ab, was nicht in der Quelle liegt.' } } if (-not (& $has 'R')) { & $add 'Warning' 'R' 'Ohne /R wiederholt robocopy eine gesperrte Datei 1 Million Mal (je 30 s) - typischerweise /R:3 /W:5 setzen.' } if ((& $has 'MON') -or (& $has 'MOT')) { & $add 'Warning' ($(if (& $has 'MON') { 'MON' } else { 'MOT' })) 'Mit /MON bzw. /MOT laeuft robocopy dauerhaft weiter und endet nicht von selbst.' } if (& $has 'REG') { & $add 'Warning' 'REG' '/REG aendert die robocopy-Standardwerte in der Registrierung fuer alle kuenftigen Aufrufe.' } $admin = @($o.Keys | Where-Object { $lookup[$_].Admin }) if ((& $has 'COPY') -and $o['COPY'] -match '[OU]') { $admin += 'COPY' } if ($admin.Count) { & $add 'Warning' $admin[0] "$((@($admin) | ForEach-Object { '/' + $_ }) -join ', ') braucht Administrator- bzw. Sicherungsrechte." } $issues.ToArray() } |