# Sprawdzanie pustych katalogów (natychmiastowo i rekursywnie) # Autor: @ai (Abisal) # Działanie: # - Skanuje wszystkie katalogi od ROOT # - Wylicza: IsEmptyImmediate (bezpośrednio), IsEmptyDeep (rekursywnie) # - Zapisuje wyniki do CSV i log TXT # - (Opcjonalnie) usuwa rekursywnie puste katalogi (IsEmptyDeep) # --- USTAWIENIA --- $RootShare = '\\192.168.0.10\photo.abisal.pl' $OutCsv = '\\192.168.0.10\photo.abisal.pl\_ARCHIWUM_\empty_dirs_report.csv' $OutLog = '\\192.168.0.10\photo.abisal.pl\_ARCHIWUM_\empty_dirs_log.txt' $IncludeHiddenAndSystem = $true # jeżeli $false, pomija ukryte/systemowe elementy przy ocenie pustki # --- FUNKCJE NISKIEGO POZIOMU --- function Test-HasEntriesImmediate { param( [Parameter(Mandatory=$true)][string]$Dir, [bool]$IncludeHiddenAndSystem = $true ) try { # Szybki test obecności czegokolwiek wewnątrz (pliki lub katalogi) if ($IncludeHiddenAndSystem) { $enum = [System.IO.Directory]::EnumerateFileSystemEntries($Dir) } else { # Filtrowanie: bez Hidden/System (bazuje na atrybutach) $enum = [System.IO.Directory]::EnumerateFileSystemEntries($Dir) | Where-Object { $attrs = (Get-Item -LiteralPath $_ -ErrorAction SilentlyContinue).Attributes -not ($attrs -band [IO.FileAttributes]::Hidden) -and -not ($attrs -band [IO.FileAttributes]::System) } } $e = $enum.GetEnumerator() $hasAny = $e.MoveNext() return $hasAny } catch { throw } } function Get-Depth { param([string]$Path) # Głębokość: liczba separatorów (dla UNC używamy '\') return ($Path.TrimEnd('\') -split '\\').Count } # --- START --- New-Item -ItemType Directory -Force -Path (Split-Path $OutCsv) | Out-Null "START $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" | Out-File -FilePath $OutLog -Encoding UTF8 if (-not (Test-Path -LiteralPath $RootShare -PathType Container)) { "ERROR: ROOT nie istnieje: $RootShare" | Out-File -FilePath $OutLog -Append -Encoding UTF8 throw "Root path not found: $RootShare" } # Pobierz WSZYSTKIE katalogi (włącznie z samym ROOT) $allDirs = New-Object System.Collections.Generic.List[System.IO.DirectoryInfo] $rootInfo = Get-Item -LiteralPath $RootShare -ErrorAction Stop $allDirs.Add($rootInfo) Get-ChildItem -LiteralPath $RootShare -Recurse -Directory -ErrorAction SilentlyContinue | ForEach-Object { $allDirs.Add($_) } # Posortuj malejąco po głębokości (bottom-up), żeby policzyć IsEmptyDeep wydajnie $dirsByDepth = $allDirs | Sort-Object { Get-Depth $_.FullName } -Descending $emptyDeepMap = @{} # Hashtable: pełna ścieżka -> $true/$false (czy rekursywnie pusty) $results = New-Object System.Collections.Generic.List[pscustomobject] $checked = 0 $errors = 0 foreach ($dirInfo in $dirsByDepth) { $checked++ $path = $dirInfo.FullName $depth = Get-Depth $path try { # 1) Pustka natychmiastowa (zero wpisów w katalogu) $hasImmediate = Test-HasEntriesImmediate -Dir $path -IncludeHiddenAndSystem:$IncludeHiddenAndSystem $isEmptyImmediate = -not $hasImmediate # 2) Pustka głęboka (rekursywna): # - nie ma plików bezpośrednio w katalogu # - wszystkie podkatalogi są IsEmptyDeep = $true $hasDirectFiles = [System.IO.Directory]::EnumerateFiles($path) | Select-Object -First 1 | ForEach-Object { $true } if (-not $hasDirectFiles) { $hasDirectFiles = $false } $subdirs = [System.IO.Directory]::EnumerateDirectories($path) $allSubdirsEmpty = $true foreach ($sd in $subdirs) { if ($emptyDeepMap.ContainsKey($sd)) { if (-not $emptyDeepMap[$sd]) { $allSubdirsEmpty = $false; break } } else { # Brak danych (np. błąd dostępu wcześniej) – na wszelki wypadek uznaj jako NIEpusty $allSubdirsEmpty = $false; break } } $isEmptyDeep = (-not $hasDirectFiles) -and $allSubdirsEmpty # Zapamiętaj dla wyższych poziomów $emptyDeepMap[$path] = $isEmptyDeep # Zapis do wyników $results.Add([pscustomobject]@{ Path = $path Depth = $depth IsEmptyImmediate = $isEmptyImmediate IsEmptyDeep = $isEmptyDeep Error = $null }) | Out-Null } catch { $errors++ $msg = $_.Exception.Message "ERROR | $path | $msg" | Out-File -FilePath $OutLog -Append -Encoding UTF8 $emptyDeepMap[$path] = $false $results.Add([pscustomobject]@{ Path = $path Depth = $depth IsEmptyImmediate = $false IsEmptyDeep = $false Error = $msg }) | Out-Null } } # Zapis raportu $results | Sort-Object Depth, Path | Export-Csv -Path $OutCsv -NoTypeInformation -Encoding UTF8 # Podsumowanie $cntImmediate = ($results | Where-Object { $_.IsEmptyImmediate -and -not $_.Error }).Count $cntDeep = ($results | Where-Object { $_.IsEmptyDeep -and -not $_.Error }).Count "SUMMARY | Checked=$checked | Errors=$errors | EmptyImmediate=$cntImmediate | EmptyDeep=$cntDeep" | Out-File -FilePath $OutLog -Append -Encoding UTF8 Write-Host "Gotowe. Raport: $OutCsv. Puste (natychmiast): $cntImmediate, puste (rekursywnie): $cntDeep. Log: $OutLog" # --- (OPCJONALNE) USUNIĘCIE rekursywnie pustych katalogów --- # UWAGA: Najpierw zrób przegląd CSV! Gdy gotowy – odkomentuj poniższą sekcję i usuń -WhatIf. <# $toDelete = $results | Where-Object { $_.IsEmptyDeep -and -not $_.Error } | Sort-Object Depth -Descending foreach ($row in $toDelete) { try { Remove-Item -LiteralPath $row.Path -Force -Recurse -WhatIf "DELETE-DRYRUN | $($row.Path)" | Out-File -FilePath $OutLog -Append -Encoding UTF8 } catch { "DELETE-ERROR | $($row.Path) | $($_.Exception.Message)" | Out-File -FilePath $OutLog -Append -Encoding UTF8 } } #>