Functions/GenXdev.FileSystem/Remove-OnReboot.ps1
################################################################################ <# .SYNOPSIS Marks a file for deletion on next system boot using Windows API. .DESCRIPTION Items are renamed to a temporary filename first to handle locked files. All moves are tracked to maintain file system links. .PARAMETER Path The path(s) to the files or directories to mark for deletion. .PARAMETER MarkInPlace Marks the file for deletion even if renaming it fails. .EXAMPLE Remove-OnReboot -Path "C:\temp\locked-file.txt" .EXAMPLE "file1.txt","file2.txt" | Remove-OnReboot #> function Remove-OnReboot { [CmdletBinding(SupportsShouldProcess)] [OutputType([bool])] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [string[]]$Path, ############################################################################### [Parameter(Mandatory = $false, HelpMessage = "Marks the file for deletion even if renaming it fails")] [switch]$MarkInPlace ############################################################################### ) begin { # Registry key for pending file operations $regKey = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" $regName = "PendingFileRenameOperations" # Get existing pending renames or create new array try { $pendingRenames = @(Get-ItemProperty -Path $regKey -Name $regName -ErrorAction SilentlyContinue).$regName } catch { $pendingRenames = @() } if ($null -eq $pendingRenames) { $pendingRenames = @() } } process { try { foreach ($item in $Path) { $fullPath = Expand-Path $item if (Test-Path -LiteralPath $fullPath) { if ($PSCmdlet.ShouldProcess($fullPath, "Mark for deletion on reboot")) { try { # Try direct deletion first Remove-Item -LiteralPath $fullPath -Force -ErrorAction Stop Write-Verbose "Successfully deleted: $fullPath" continue } catch { Write-Verbose "Direct deletion failed for $fullPath, attempting rename and mark for deletion..." try { # Get directory and generate new hidden name $newName = "." + [System.Guid]::NewGuid().ToString() $newPath = [System.IO.Path]::Combine($dir, $newName) # Rename the file and set attributes Rename-Item -Path $fullPath -NewName $newName -Force -ErrorAction Stop $file = Get-Item -LiteralPath $newPath -Force $file.Attributes = $file.Attributes -bor [System.IO.FileAttributes]::Hidden -bor [System.IO.FileAttributes]::System Write-Verbose "Renamed to hidden system file: $newPath" # Format paths with \??\ prefix for registry $sourcePath = "\??\" + $newPath $pendingRenames += $sourcePath $pendingRenames += "" Write-Verbose "Marked for deletion on reboot: $newPath" } catch { if ($MarkInPlace) { Write-Verbose "Renaming failed, marking in place for deletion on reboot: $fullPath" $sourcePath = "\??\" + $fullPath $pendingRenames += $sourcePath $pendingRenames += "" } else { Write-Error "Failed to rename $($fullPath): $_" continue } } } } } else { Write-Warning "Path not found: $fullPath" } } if ($pendingRenames.Count -gt 0) { # Save as REG_MULTI_SZ Set-ItemProperty -Path $regKey -Name $regName -Value $pendingRenames -Type MultiString return $true } } catch { Write-Error "Failed to set pending file operations: $_" return $false } return $true } } |