Public/Invoke-sqmFormatDrive64k.ps1
|
<# .SYNOPSIS Checks an NTFS drive for 64 KB allocation unit size and formats it with 65536 byte cluster size if needed. .DESCRIPTION Process: 1. Safety checks (not C:, NTFS, one primary partition). 2. Save drive metadata (letter, label, partition size). 3. Check allocation unit via Get-Volume / fsutil. 4. If cluster size is already 65536 bytes -> abort with status 'AlreadyOK'. 5. Check whether the drive is in use by a process. If so: warning and abort (status 'InUse'). 6. If drive contains data: back up with robocopy to $BackupPath\<Letter>_<Timestamp>\. 7. Format-Volume with -AllocationUnitSize 65536 -FileSystem NTFS. 8. Restore drive letter and label. 9. If data was backed up: restore with robocopy. Restore error -> warning, backup remains on C:. 10. Delete backup on C: only if robocopy restored without errors. Safety rules: - Drive C: is never formatted (hard-coded guard). - Only NTFS volumes are accepted. - Only drives with exactly one primary partition. - Drives opened by a process -> abort. .PARAMETER DriveLetter Target drive letter (single letter, e.g. 'D'). Mandatory. C is explicitly prohibited. .PARAMETER BackupPath Temporary backup path on C: for data backup before formatting. Default: C:\Temp\DriveBackup. Must reside on drive C:. .PARAMETER Force Skips the interactive confirmation prompt before formatting. .PARAMETER WhatIf Simulates all steps without making changes. .PARAMETER Confirm Requests explicit confirmation before formatting. .OUTPUTS [PSCustomObject] with the following fields: DriveLetter : Drive letter Label : Drive label PreviousClusterSize : Cluster size before the action (bytes) NewClusterSize : Cluster size after the action (bytes) DataBackedUp : $true if data was backed up BackupFolder : Path of the backup (or $null) DataRestored : $true if data was successfully restored BackupCleanedUp : $true if backup on C: was deleted after restore Status : AlreadyOK | Formatted | InUse | Error | WhatIf Message : Detail message .EXAMPLE Invoke-sqmFormatDrive64k -DriveLetter D Checks drive D: and formats it with 64 KB clusters if needed. Data is backed up to C:\Temp\DriveBackup first. .EXAMPLE Invoke-sqmFormatDrive64k -DriveLetter E -BackupPath "C:\Backup\DriveTemp" -Force Same as above, without confirmation prompt, using a different backup path. .EXAMPLE Invoke-sqmFormatDrive64k -DriveLetter D -WhatIf Simulates the entire process without making any changes. .NOTES Prerequisites : Windows PowerShell 5.1 or PowerShell 7+, local administrator rights, robocopy.exe (part of Windows). Cluster size : 65536 bytes = 64 KB = optimal setting for SQL Server data files (Microsoft recommendation). Robocopy flags : /E all subdirectories including empty ones /COPYALL all file attributes, timestamps, ACLs, streams /R:3 max. 3 retries per file /W:5 5 seconds wait between retries /NP no progress display in % /LOG log file next to backup folder Robocopy exit codes 0-3 are treated as success (0=ok, 1=new files, 2=extra, 3=both). From 4 onwards a warning is issued but execution is not necessarily aborted. #> 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 } |