bin/Public/Invoke-sqmFormatDrive64k.ps1
|
<# .SYNOPSIS Prueft ein NTFS-Laufwerk auf 64 KB-Allokationseinheit und formatiert es bei Bedarf mit 65536 Byte Clustergroesse. .DESCRIPTION Ablauf: 1. Sicherheitspruefungen (kein C:, NTFS, eine primaere Partition). 2. Laufwerk-Metadaten sichern (Buchstabe, Label, Partitionsgroesse). 3. Pruefung der Allokationseinheit via Get-Volume / fsutil. 4. Ist die Clustergroesse bereits 65536 Byte ? Abbruch mit Status 'AlreadyOK'. 5. Pruefung ob das Laufwerk von einem Prozess verwendet wird. Falls ja: Warnung und Abbruch (Status 'InUse'). 6. Enthaelt das Laufwerk Daten: Sicherung mit robocopy nach $BackupPath\<Buchstabe>_<Zeitstempel>\. 7. Format-Volume mit -AllocationUnitSize 65536 -FileSystem NTFS. 8. Laufwerksbuchstaben und Label wiederherstellen. 9. Falls Daten gesichert: Rueckspielen mit robocopy. Fehler beim Rueckspielen ? Warnung, Backup bleibt auf C: erhalten. 10. Backup auf C: nur loeschen wenn robocopy fehlerfrei zurueckgespielt hat. Sicherheitsregeln: - Laufwerk C: wird niemals formatiert (hartkodierter Guard). - Nur NTFS-Volumes werden akzeptiert. - Nur Laufwerke mit genau einer primaeren Partition. - Laufwerke die von einem Prozess geoeffnet sind ? Abbruch. .PARAMETER DriveLetter Ziellaufwerksbuchstabe (einzelner Buchstabe, z. B. 'D'). Pflichtparameter. C ist explizit verboten. .PARAMETER BackupPath Temporaerer Sicherungspfad auf C: fuer Datensicherung vor dem Format. Standard: C:\Temp\DriveBackup. Muss auf Laufwerk C: liegen. .PARAMETER Force ueberspringt die interaktive Bestaetigungsabfrage vor dem Formatieren. .PARAMETER WhatIf Simuliert alle Schritte ohne aenderungen durchzufuehren. .PARAMETER Confirm Fordert vor dem Formatieren eine explizite Bestaetigung an. .OUTPUTS [PSCustomObject] mit folgenden Feldern: DriveLetter : Laufwerksbuchstabe Label : Laufwerk-Label PreviousClusterSize : Clustergroesse vor der Aktion (Byte) NewClusterSize : Clustergroesse nach der Aktion (Byte) DataBackedUp : $true wenn Daten gesichert wurden BackupFolder : Pfad des Backups (oder $null) DataRestored : $true wenn Daten erfolgreich zurueckgespielt wurden BackupCleanedUp : $true wenn Backup auf C: nach Restore geloescht wurde Status : AlreadyOK | Formatted | InUse | Error | WhatIf Message : Detailmeldung .EXAMPLE Invoke-sqmFormatDrive64k -DriveLetter D Prueft Laufwerk D: und formatiert es bei Bedarf mit 64 KB-Clustern. Daten werden vorher nach C:\Temp\DriveBackup gesichert. .EXAMPLE Invoke-sqmFormatDrive64k -DriveLetter E -BackupPath "C:\Backup\DriveTemp" -Force Wie oben, ohne Bestaetigungsabfrage, abweichender Backup-Pfad. .EXAMPLE Invoke-sqmFormatDrive64k -DriveLetter D -WhatIf Simuliert den gesamten Ablauf ohne aenderungen. .NOTES Voraussetzungen : Windows PowerShell 5.1 oder PowerShell 7+, lokale Administratorrechte, robocopy.exe (Bestandteil von Windows). Clustergroesse : 65536 Byte = 64 KB = optimale Einstellung fuer SQL Server- Datendateien (Microsoft-Empfehlung). Robocopy-Flags : /E alle Unterverzeichnisse inkl. leere /COPYALL alle Dateiattribute, Timestamps, ACLs, Streams /R:3 max. 3 Wiederholungen pro Datei /W:5 5 Sekunden Wartezeit zwischen Wiederholungen /NP keine Fortschrittsanzeige in % /LOG Protokolldatei neben Backup-Ordner Robocopy Exit-Codes 0-3 gelten als Erfolg (0=ok, 1=neue Dateien, 2=extra, 3=beides). Ab 4 wird gewarnt aber nicht zwingend abgebrochen. #> function Invoke-sqmFormatDrive64k { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, Position = 0)] [ValidatePattern('^[A-Za-z]$')] [string]$DriveLetter, [Parameter(Mandatory = $false)] [string]$BackupPath = 'C:\Temp\DriveBackup', [Parameter(Mandatory = $false)] [switch]$Force ) # ???????????????????????????????????????????????????????????????????????? # Initialisierung # ???????????????????????????????????????????????????????????????????????? $functionName = $MyInvocation.MyCommand.Name $DriveLetter = $DriveLetter.ToUpper() $drivePath = "$DriveLetter`:" $targetCluster = 65536 # 64 KB # Ergebnis-Objekt (wird laufend befuellt) $result = [PSCustomObject]@{ DriveLetter = $DriveLetter Label = $null PreviousClusterSize = $null NewClusterSize = $null DataBackedUp = $false BackupFolder = $null DataRestored = $false BackupCleanedUp = $false Status = 'Error' Message = $null } # ?? Logging-Hilfsfunktion (kapselt Invoke-sqmLogging) ?????????????????? function _Log { param ([string]$Msg, [string]$Level = 'INFO') Write-Verbose "[$functionName] $Msg" try { Invoke-sqmLogging -Message "[$drivePath] $Msg" -FunctionName $functionName -Level $Level } catch { } # Logging-Fehler nie weiterpropagieren } # ???????????????????????????????????????????????????????????????????????? # SCHRITT 1 - Sicherheitspruefungen # ???????????????????????????????????????????????????????????????????????? # C: ist absolut verboten if ($DriveLetter -eq 'C') { $msg = "Laufwerk C: darf niemals formatiert werden. Abbruch." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } # BackupPath muss auf C: liegen if ($BackupPath -notmatch '^[Cc]:\\') { $msg = "BackupPath '$BackupPath' muss auf Laufwerk C: liegen." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } # Administratorrechte pruefen $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { $msg = "Das Skript erfordert lokale Administratorrechte." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } # Laufwerk vorhanden? $volume = Get-Volume -DriveLetter $DriveLetter -ErrorAction SilentlyContinue if (-not $volume) { $msg = "Laufwerk $drivePath nicht gefunden." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } # Nur NTFS if ($volume.FileSystem -ne 'NTFS') { $msg = "Laufwerk $drivePath hat Dateisystem '$($volume.FileSystem)' - nur NTFS wird unterstuetzt." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } # Genau eine primaere Partition $partitions = Get-Partition -DriveLetter $DriveLetter -ErrorAction SilentlyContinue | Where-Object { $_.Type -eq 'Basic' -or $_.MbrType -eq 7 -or $_.GptType } # Partition ueber DriveLetter holen (robuster Weg) $partition = Get-Partition -DriveLetter $DriveLetter -ErrorAction SilentlyContinue if (-not $partition) { $msg = "Keine Partition fuer Laufwerk $drivePath gefunden." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } if (@($partition).Count -gt 1) { $msg = "Laufwerk $drivePath hat mehr als eine Partition - nur Laufwerke mit einer primaeren Partition werden unterstuetzt." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } # Metadaten sichern $result.Label = $volume.FileSystemLabel _Log "Laufwerk gefunden: $drivePath | Label='$($result.Label)' | FS=$($volume.FileSystem) | Groesse=$([math]::Round($volume.Size/1GB,2)) GB" # ???????????????????????????????????????????????????????????????????????? # SCHRITT 2 - Aktuelle Clustergroesse ermitteln (fsutil) # ???????????????????????????????????????????????????????????????????????? _Log "Ermittle Clustergroesse via fsutil ..." try { $fsutilOut = & fsutil fsinfo ntfsinfo "$drivePath\" 2>&1 $clusterLine = $fsutilOut | Where-Object { $_ -match 'Bytes Per Cluster' } if ($clusterLine -match ':\s+(\d+)') { $currentCluster = [int]$Matches[1] } else { # Fallback: Get-Volume liefert AllocationUnitSize ab Win10/2016 $currentCluster = (Get-Volume -DriveLetter $DriveLetter).AllocationUnitSize } } catch { $msg = "Clustergroesse konnte nicht ermittelt werden: $($_.Exception.Message)" _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } $result.PreviousClusterSize = $currentCluster _Log "Aktuelle Clustergroesse: $currentCluster Byte ($([math]::Round($currentCluster/1KB,0)) KB)" # Bereits korrekt formatiert? if ($currentCluster -eq $targetCluster) { $msg = "Laufwerk $drivePath ist bereits mit 64 KB-Clustern formatiert. Keine Aktion erforderlich." _Log $msg 'INFO' $result.NewClusterSize = $currentCluster $result.Status = 'AlreadyOK' $result.Message = $msg Write-Host $msg -ForegroundColor Green return $result } _Log "Clustergroesse $currentCluster Byte ? $targetCluster Byte - Formatierung erforderlich." 'WARNING' # ???????????????????????????????????????????????????????????????????????? # SCHRITT 3 - Laufwerk in Benutzung? # ???????????????????????????????????????????????????????????????????????? _Log "Pruefe ob $drivePath von Prozessen verwendet wird ..." $openHandles = $false try { # openfiles.exe erfordert 'Maintain Objects List' - zuverlaessiger: handle via # WMI CIM_Process + Get-Process mit Modulpfad-Filter $busyProcesses = Get-Process -ErrorAction SilentlyContinue | Where-Object { try { $_.Modules | Where-Object { $_.FileName -like "$drivePath\*" } } catch { $false } } # Zusaetzlich: alle offenen Datei-Handles ueber .NET (schnell, ohne Sysinternals) $drivePrefix = "$drivePath\" $appDomainCheck = [System.IO.Directory]::GetFiles($drivePrefix, '*', [System.IO.SearchOption]::TopDirectoryOnly) 2>$null if ($busyProcesses) { $procList = ($busyProcesses | Select-Object -ExpandProperty Name -Unique) -join ', ' $msg = "Laufwerk $drivePath wird von folgenden Prozessen verwendet: $procList - Abbruch." _Log $msg 'WARNING' Write-Warning $msg $result.Status = 'InUse' $result.Message = $msg return $result } } catch { _Log "Prozess-Pruefung: $($_.Exception.Message) - wird fortgesetzt." 'WARNING' } # ???????????????????????????????????????????????????????????????????????? # SCHRITT 4 - Daten vorhanden? ? Backup mit robocopy # ???????????????????????????????????????????????????????????????????????? $hasData = $false $backupFolder = $null $robocopyLog = $null try { $items = Get-ChildItem -Path "$drivePath\" -Force -ErrorAction SilentlyContinue $hasData = [bool]$items } catch { $hasData = $false } if ($hasData) { $timestamp = Get-Date -Format 'yyyyMMdd_HHmsqm' $backupFolder = Join-Path $BackupPath "${DriveLetter}_${timestamp}" $robocopyLog = Join-Path $BackupPath "${DriveLetter}_${timestamp}_backup.log" $result.BackupFolder = $backupFolder _Log "Laufwerk enthaelt Daten - Sicherung nach '$backupFolder' ..." if ($PSCmdlet.ShouldProcess($drivePath, "Daten nach '$backupFolder' sichern (robocopy)")) { if (-not (Test-Path $BackupPath)) { New-Item -ItemType Directory -Path $BackupPath -Force -ErrorAction Stop | Out-Null _Log "Backup-Verzeichnis '$BackupPath' erstellt." } $rcArgs = @( "$drivePath\", # Quelle $backupFolder, # Ziel '/E', # alle Unterverzeichnisse inkl. leere '/COPYALL', # Attribute, Timestamps, ACLs, Streams '/R:3', # max. 3 Wiederholungen '/W:5', # 5 s Wartezeit '/NP', # keine %-Fortschrittsanzeige '/LOG+:' + $robocopyLog ) _Log "Starte robocopy Backup: robocopy $($rcArgs -join ' ')" & robocopy @rcArgs | Out-Null $rcExit = $LASTEXITCODE if ($rcExit -le 3) { $result.DataBackedUp = $true _Log "Backup erfolgreich (robocopy ExitCode $rcExit)." } else { $msg = "Backup fehlgeschlagen (robocopy ExitCode $rcExit). Log: $robocopyLog - Abbruch." _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } } else { _Log "WhatIf: Backup wuerde nach '$backupFolder' erstellt." 'INFO' } } else { _Log "Laufwerk $drivePath enthaelt keine Daten - kein Backup erforderlich." } # ???????????????????????????????????????????????????????????????????????? # SCHRITT 5 - Bestaetigung und Format # ???????????????????????????????????????????????????????????????????????? $confirmMsg = "Laufwerk $drivePath (Label: '$($result.Label)') wird mit 64 KB-Clustern formatiert. " + "ALLE DATEN WERDEN GELoeSCHT$(if ($result.DataBackedUp) { " (Backup: $backupFolder)" } else { '' })." if (-not $Force -and -not $PSCmdlet.ShouldProcess($drivePath, $confirmMsg)) { $msg = "Formatierung durch Benutzer abgebrochen." _Log $msg 'WARNING' $result.Status = 'Error' $result.Message = $msg return $result } _Log "Formatiere $drivePath mit 65536 Byte Clustergroesse, NTFS, Label='$($result.Label)' ..." if (-not $WhatIfPreference) { try { Format-Volume ` -DriveLetter $DriveLetter ` -FileSystem NTFS ` -AllocationUnitSize $targetCluster ` -NewFileSystemLabel $result.Label ` -Force ` -Confirm:$false ` -ErrorAction Stop | Out-Null } catch { $msg = "Format-Volume fehlgeschlagen: $($_.Exception.Message)" _Log $msg 'ERROR' $result.Status = 'Error' $result.Message = $msg Write-Error $msg return $result } # Laufwerksbuchstaben wiederherstellen (Format-Volume entfernt ihn manchmal nicht, # aber zur Sicherheit explizit setzen) try { $newPartition = Get-Partition -DriveLetter $DriveLetter -ErrorAction SilentlyContinue if (-not $newPartition) { # Buchstabe neu zuweisen $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop $freshPartition = $disk | Get-Partition | Where-Object { $_.PartitionNumber -eq $partition.PartitionNumber } if ($freshPartition) { Set-Partition -InputObject $freshPartition -NewDriveLetter $DriveLetter -ErrorAction Stop _Log "Laufwerksbuchstabe $drivePath wiederhergestellt." } } } catch { _Log "Laufwerksbuchstabe konnte nicht wiederhergestellt werden: $($_.Exception.Message)" 'WARNING' Write-Warning "Laufwerksbuchstabe ${drivePath}: $($_.Exception.Message)" } # Neue Clustergroesse verifizieren try { $fsutilNew = & fsutil fsinfo ntfsinfo "$drivePath\" 2>&1 $clusterNew = ($fsutilNew | Where-Object { $_ -match 'Bytes Per Cluster' }) -replace '.*:\s+', '' -as [int] if (-not $clusterNew) { $clusterNew = (Get-Volume -DriveLetter $DriveLetter).AllocationUnitSize } $result.NewClusterSize = $clusterNew _Log "Neue Clustergroesse verifiziert: $clusterNew Byte." } catch { _Log "Neue Clustergroesse konnte nicht verifiziert werden: $($_.Exception.Message)" 'WARNING' $result.NewClusterSize = $targetCluster # Annahme } } else { _Log "WhatIf: Format-Volume $drivePath -AllocationUnitSize 65536 -FileSystem NTFS -Label '$($result.Label)'" $result.NewClusterSize = $targetCluster $result.Status = 'WhatIf' $result.Message = "WhatIf: Keine aenderungen durchgefuehrt." return $result } # ???????????????????????????????????????????????????????????????????????? # SCHRITT 6 - Daten zurueckspielen # ???????????????????????????????????????????????????????????????????????? if ($result.DataBackedUp -and $backupFolder) { $robocopyRestoreLog = Join-Path $BackupPath "${DriveLetter}_${timestamp}_restore.log" _Log "Spiele Daten von '$backupFolder' zurueck nach $drivePath ..." $rcRestoreArgs = @( $backupFolder, # Quelle "$drivePath\", # Ziel '/E', '/COPYALL', '/R:3', '/W:5', '/NP', '/LOG+:' + $robocopyRestoreLog ) _Log "Starte robocopy Restore: robocopy $($rcRestoreArgs -join ' ')" & robocopy @rcRestoreArgs | Out-Null $rcRestoreExit = $LASTEXITCODE if ($rcRestoreExit -le 3) { $result.DataRestored = $true _Log "Restore erfolgreich (robocopy ExitCode $rcRestoreExit)." # ?? Backup auf C: bereinigen ?????????????????????????????????? _Log "Loesche Backup '$backupFolder' auf C: ..." try { Remove-Item -Path $backupFolder -Recurse -Force -ErrorAction Stop $result.BackupCleanedUp = $true _Log "Backup '$backupFolder' erfolgreich geloescht." } catch { $warnMsg = "Backup '$backupFolder' konnte nicht geloescht werden: $($_.Exception.Message)" _Log $warnMsg 'WARNING' Write-Warning $warnMsg } } else { $warnMsg = "Restore teilweise fehlgeschlagen (robocopy ExitCode $rcRestoreExit). " + "Backup bleibt erhalten: '$backupFolder'. Log: $robocopyRestoreLog" _Log $warnMsg 'WARNING' Write-Warning $warnMsg # DataRestored bleibt $false, BackupCleanedUp bleibt $false # Backup wird NICHT geloescht } } # ???????????????????????????????????????????????????????????????????????? # Abschluss # ???????????????????????????????????????????????????????????????????????? $result.Status = 'Formatted' $result.Message = "Laufwerk $drivePath erfolgreich auf 64 KB-Clustergroesse formatiert." + $(if ($result.DataRestored) { " Daten wiederhergestellt." }) + $(if ($result.BackupCleanedUp) { " Backup auf C: bereinigt." }) + $(if ($result.DataBackedUp -and -not $result.DataRestored) { " WARNUNG: Backup auf C: nicht geloescht ($backupFolder)." }) _Log $result.Message 'INFO' Write-Host $result.Message -ForegroundColor $(if ($result.DataBackedUp -and -not $result.DataRestored) { 'Yellow' } else { 'Green' }) return $result } |