# Version date: 2026-08-03 16:57 (CEST) # ------------------------------------------------------------ # Manual cleanup script - run by hand, as needed, when C: is getting full. # Unlike ShutdownCleanup.ps1 (which runs unattended and only touches # disposable cache), this script deletes real user files - so it always # shows exactly what it's about to delete first, and requires explicit # confirmation before touching anything. # # For every real local user profile on this machine, this script finds: # - Every file in the user's Downloads folder. # - Every .lif and .lifext file (Leica microscopy data) under the user's # Documents and Desktop folders. Users are not supposed to store data # on the system drive C:\ at all, so a local .lif/.lifext file found # here is a policy violation. # # A log of what was reviewed and deleted is written to FreeSpaceOnC.log, # in the same folder as this script. # ------------------------------------------------------------ # Verify this is running with Administrator rights; stop with a clear # message if not, since this needs to reach into every user's profile. if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "ERROR: Run this script as Administrator." ; exit } # -------------------------------------------------------------------------- # SETTINGS - adjust these as needed. # -------------------------------------------------------------------------- $LifExtensions = @("*.lif", "*.lifext") $LifScanFolders = @("Documents", "Desktop") # -------------------------------------------------------------------------- $LogFile = Join-Path $PSScriptRoot "FreeSpaceOnC.log" $AdminGroupName = ([System.Security.Principal.SecurityIdentifier]"S-1-5-32-544").Translate([System.Security.Principal.NTAccount]).Value # takeown's /D confirmation switch expects a localized letter - "Y" (Yes) on # English Windows, but "J" (Ja) on German Windows. This detects the current # UI language and picks the right one; if this system uses a different # language where neither applies, this may need to be edited by hand. $TakeownConfirm = if ((Get-UICulture).TwoLetterISOLanguageName -ieq "de") { "J" } else { "Y" } # This log accumulates across every run of this script (Start-Transcript # -Append below adds to it if it already exists, or creates it fresh if # not). try { Stop-Transcript -ErrorAction Stop | Out-Null } catch { } Start-Transcript -Path $LogFile -Append Write-Host "" Write-Host "========================================" Write-Host "FreeSpaceOnC scan started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-Host "========================================" # Accounts to skip - not real user profiles. $ExcludedUsers = @( "Administrator", "Guest", "Gast", "DefaultAccount", "WDAGUtilityAccount", "defaultuser0", "Public" ) $UserFolders = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin $ExcludedUsers } # -------------------------------------------------------------------------- # Phase 1: find every candidate file, across all users. Nothing is deleted # in this phase - just a list is built up for review. # -------------------------------------------------------------------------- $CandidateFiles = @() foreach ($UserFolder in $UserFolders) { # Even running elevated, an Administrator token does not always # guarantee full access into another user's folder - on some machines, # a folder can have ownership/permissions that block access until # ownership is explicitly taken, the same thing Windows Explorer does # behind the scenes when it shows the "You don't have permission... # Continue?" prompt. Do that proactively here, scoped to just the # specific folders this script actually needs, so nothing is silently # skipped due to a permission block. Modify (not just Read) is granted, # since this script needs to delete files here, not just read them. $FoldersToUnlock = @("Downloads") + $LifScanFolders foreach ($FolderName in $FoldersToUnlock) { $FolderPath = Join-Path $UserFolder.FullName $FolderName if (Test-Path $FolderPath) { takeown /F $FolderPath /R /D $TakeownConfirm | Out-Null icacls $FolderPath /grant "${AdminGroupName}:(OI)(CI)M" /T /C | Out-Null } } $DownloadsPath = Join-Path $UserFolder.FullName "Downloads" if (Test-Path $DownloadsPath) { $CandidateFiles += Get-ChildItem -Path $DownloadsPath -File -Recurse -Force -ErrorAction SilentlyContinue } foreach ($FolderName in $LifScanFolders) { $FolderPath = Join-Path $UserFolder.FullName $FolderName if (Test-Path $FolderPath) { foreach ($Ext in $LifExtensions) { $CandidateFiles += Get-ChildItem -Path $FolderPath -Filter $Ext -File -Recurse -Force -ErrorAction SilentlyContinue } } } } if ($CandidateFiles.Count -eq 0) { Write-Host "" Write-Host "Nothing found to delete." Stop-Transcript exit } # -------------------------------------------------------------------------- # Phase 2: show every file found, with full path, last-modified date, and # size, then ask how to proceed - delete all at once, review one by one, # or delete nothing. # -------------------------------------------------------------------------- Write-Host "" Write-Host "The following $($CandidateFiles.Count) file(s) will be deleted:" Write-Host "" $CandidateFiles | Sort-Object FullName | ForEach-Object { $SizeMB = [Math]::Round($_.Length / 1MB, 1) Write-Host "$($_.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')) $($SizeMB.ToString().PadLeft(8)) MB $($_.FullName)" } $TotalSizeMB = [Math]::Round((($CandidateFiles | Measure-Object Length -Sum).Sum) / 1MB, 1) Write-Host "" Write-Host "Total: $($CandidateFiles.Count) file(s), $TotalSizeMB MB" Write-Host "" Write-Host "How do you wish to proceed?" $Mode = Read-Host "(A = delete all, 1 = go through one by one, N = delete nothing)" $FilesDeleted = 0 $FilesSkipped = 0 $BytesDeleted = 0 $Errors = 0 # -------------------------------------------------------------------------- # Mode A: delete every file shown above, all at once - still requires # typing DELETE, since this is the fast, irreversible, no-further-review # option. # -------------------------------------------------------------------------- if ($Mode -ieq "A") { Write-Host "" Write-Host "This cannot be undone. Type DELETE (all caps) to proceed." $Confirm = Read-Host "Confirm" if ($Confirm -cne "DELETE") { Write-Host "Aborted by user - nothing was deleted." Stop-Transcript exit } foreach ($File in $CandidateFiles) { try { Remove-Item -Path $File.FullName -Force -ErrorAction Stop $FilesDeleted++ $BytesDeleted += $File.Length } catch { Write-Host "ERROR deleting $($File.FullName): $($_.Exception.Message)" $Errors++ } } } # -------------------------------------------------------------------------- # Mode 1: go through each file individually - a simple Y/N per file is # enough here, since choosing this mode already signals a deliberate, # careful review rather than a one-shot bulk action. # -------------------------------------------------------------------------- elseif ($Mode -eq "1") { foreach ($File in ($CandidateFiles | Sort-Object FullName)) { $SizeMB = [Math]::Round($File.Length / 1MB, 1) Write-Host "" Write-Host "$($File.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')) $SizeMB MB $($File.FullName)" $ItemConfirm = Read-Host "Delete this file? (Y/N)" if ($ItemConfirm -ieq "Y") { try { Remove-Item -Path $File.FullName -Force -ErrorAction Stop $FilesDeleted++ $BytesDeleted += $File.Length } catch { Write-Host "ERROR deleting $($File.FullName): $($_.Exception.Message)" $Errors++ } } else { $FilesSkipped++ } } } # -------------------------------------------------------------------------- # Anything else (including N): delete nothing. # -------------------------------------------------------------------------- else { Write-Host "Aborted by user - nothing was deleted." Stop-Transcript exit } Write-Host "" Write-Host "========================================" Write-Host "Deletion summary" Write-Host "========================================" Write-Host "Files deleted : $FilesDeleted" Write-Host "Files skipped : $FilesSkipped" Write-Host "Space freed : $([Math]::Round($BytesDeleted / 1MB, 1)) MB" Write-Host "Errors : $Errors" Write-Host "========================================" Stop-Transcript