<# .SYNOPSIS Zips up folders that haven't been touched in N days, and optionally removes the originals. .DESCRIPTION Looks at each subfolder of -Path and finds the newest file anywhere inside it. If nothing in the folder has changed for -OlderThanDays, the folder is zipped to -Destination as _.zip (the date of the last change). The ZIP is checked against the folder's file count before -RemoveSource is allowed to delete anything. Empty folders are skipped. Supports -WhatIf. .PARAMETER Path The parent folder. Each folder directly under it is considered separately. .PARAMETER Destination Where the ZIP files go. Created if it doesn't exist. .PARAMETER OlderThanDays Archive a folder only if its newest file is older than this. Default 180. .PARAMETER RemoveSource Delete each folder after its ZIP has been written and verified. .PARAMETER Force Overwrite a ZIP with the same name that's already in the destination. .EXAMPLE .\Compress-OldFolder.ps1 -Path D:\Projects -Destination E:\Archive\Projects -WhatIf .EXAMPLE .\Compress-OldFolder.ps1 -Path D:\Projects -Destination E:\Archive\Projects -OlderThanDays 365 -RemoveSource #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })][string]$Path, [Parameter(Mandatory)][string]$Destination, [ValidateRange(1, 36500)][int]$OlderThanDays = 180, [switch]$RemoveSource, [switch]$Force ) Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem $cutoff = (Get-Date).AddDays(-$OlderThanDays) $Destination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) foreach ($folder in Get-ChildItem -LiteralPath $Path -Directory) { $files = @(Get-ChildItem -LiteralPath $folder.FullName -File -Recurse -Force -ErrorAction SilentlyContinue) if (-not $files) { Write-Verbose "$($folder.Name): no files inside, skipped." continue } $newest = ($files | Sort-Object LastWriteTime -Descending | Select-Object -First 1).LastWriteTime if ($newest -ge $cutoff) { Write-Verbose "$($folder.Name): last change $($newest.ToString('yyyy-MM-dd')), too recent." continue } $zipPath = Join-Path $Destination ('{0}_{1:yyyy-MM-dd}.zip' -f $folder.Name, $newest) $result = [ordered]@{ Folder = $folder.Name; LastChange = $newest.Date; Files = $files.Count; SizeMB = [math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1); ZipMB = $null; Status = $null; Zip = $zipPath } if ((Test-Path -LiteralPath $zipPath) -and -not $Force) { $result.Status = 'SkippedExists' [pscustomobject]$result continue } if (-not $PSCmdlet.ShouldProcess($folder.FullName, "Archive to $zipPath")) { continue } $partial = "$zipPath.partial" try { if (-not (Test-Path -LiteralPath $Destination)) { $null = New-Item -ItemType Directory -Path $Destination } if (Test-Path -LiteralPath $partial) { Remove-Item -LiteralPath $partial -Force } # CreateFromDirectory includes hidden files and handles big archives, unlike Compress-Archive in 5.1. [System.IO.Compression.ZipFile]::CreateFromDirectory($folder.FullName, $partial, [System.IO.Compression.CompressionLevel]::Optimal, $true) $zip = [System.IO.Compression.ZipFile]::OpenRead($partial) try { $zipped = @($zip.Entries | Where-Object { $_.Name }).Count } finally { $zip.Dispose() } if ($zipped -ne $files.Count) { throw "ZIP holds $zipped files but the folder has $($files.Count)." } Move-Item -LiteralPath $partial -Destination $zipPath -Force -WhatIf:$false $result.ZipMB = [math]::Round((Get-Item -LiteralPath $zipPath).Length / 1MB, 1) $result.Status = 'Archived' if ($RemoveSource -and $PSCmdlet.ShouldProcess($folder.FullName, 'Delete folder')) { Remove-Item -LiteralPath $folder.FullName -Recurse -Force $result.Status = 'ArchivedRemoved' } } catch { Remove-Item -LiteralPath $partial -Force -ErrorAction SilentlyContinue -WhatIf:$false $result.Status = "Failed: $($_.Exception.Message)" Write-Warning "$($folder.Name): $($_.Exception.Message)" } [pscustomobject]$result }