Added version cleanup feature
This commit is contained in:
@@ -2866,6 +2866,20 @@ $script:LangDefault = @{
|
||||
"btn.struct.clear" = "Clear"
|
||||
"struct.col.path" = "Full Path"
|
||||
"struct.col.depth" = "Depth"
|
||||
"tab.versions" = " Versions "
|
||||
"grp.ver.keep" = "Versions to Keep"
|
||||
"lbl.ver.count" = "Number of versions to keep:"
|
||||
"chk.ver.date" = "Also filter by date"
|
||||
"rad.ver.before" = "Keep versions before:"
|
||||
"rad.ver.after" = "Keep versions after:"
|
||||
"grp.ver.scope" = "Scope"
|
||||
"lbl.ver.library" = "Library / Folder:"
|
||||
"ph.ver.library" = "Shared Documents"
|
||||
"chk.ver.recursive" = "Include subfolders (recursive)"
|
||||
"chk.ver.subsites" = "Include subsites"
|
||||
"chk.ver.dryrun" = "Dry run (preview only, no deletion)"
|
||||
"btn.ver.run" = "Clean Versions"
|
||||
"btn.ver.open" = "Open Report"
|
||||
}
|
||||
|
||||
$script:Lang = $null # null = use LangDefault
|
||||
@@ -3672,7 +3686,83 @@ $btnStructClear.Size = New-Object System.Drawing.Size(90, 30)
|
||||
|
||||
$tabStruct.Controls.AddRange(@($grpStructCsv, $grpStructPreview, $lblStructLib, $txtStructLib, $btnStructCreate, $btnStructClear))
|
||||
|
||||
$tabs.TabPages.AddRange(@($tabPerms, $tabStorage, $tabTemplates, $tabSearch, $tabDupes, $tabTransfer, $tabBulk, $tabStruct))
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Tab 9 – Version Cleanup
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
$tabVersions = New-Object System.Windows.Forms.TabPage
|
||||
$tabVersions.Text = T "tab.versions"
|
||||
$tabVersions.BackColor = [System.Drawing.Color]::WhiteSmoke
|
||||
|
||||
# ── Versions to keep ─────────────────────────────────────────────────────────
|
||||
$grpVerKeep = New-Group (T "grp.ver.keep") 10 4 620 110
|
||||
|
||||
$lblVerCount = New-Object System.Windows.Forms.Label
|
||||
$lblVerCount.Text = T "lbl.ver.count"
|
||||
$lblVerCount.Location = New-Object System.Drawing.Point(10, 22)
|
||||
$lblVerCount.Size = New-Object System.Drawing.Size(220, 20)
|
||||
|
||||
$nudVerCount = New-Object System.Windows.Forms.NumericUpDown
|
||||
$nudVerCount.Location = New-Object System.Drawing.Point(235, 20)
|
||||
$nudVerCount.Size = New-Object System.Drawing.Size(70, 22)
|
||||
$nudVerCount.Minimum = 0
|
||||
$nudVerCount.Maximum = 500
|
||||
$nudVerCount.Value = 5
|
||||
|
||||
$chkVerDate = New-Check (T "chk.ver.date") 10 50 250 $false
|
||||
|
||||
$radVerBefore = New-Radio (T "rad.ver.before") 30 74 200 $true
|
||||
$radVerBefore.Enabled = $false
|
||||
$radVerAfter = New-Radio (T "rad.ver.after") 30 96 200 $false
|
||||
$radVerAfter.Enabled = $false
|
||||
|
||||
$dtpVer = New-Object System.Windows.Forms.DateTimePicker
|
||||
$dtpVer.Location = New-Object System.Drawing.Point(235, 74)
|
||||
$dtpVer.Size = New-Object System.Drawing.Size(150, 22)
|
||||
$dtpVer.Format = [System.Windows.Forms.DateTimePickerFormat]::Short
|
||||
$dtpVer.Enabled = $false
|
||||
|
||||
$chkVerDate.Add_CheckedChanged({
|
||||
$on = $chkVerDate.Checked
|
||||
$radVerBefore.Enabled = $on
|
||||
$radVerAfter.Enabled = $on
|
||||
$dtpVer.Enabled = $on
|
||||
})
|
||||
|
||||
$grpVerKeep.Controls.AddRange(@($lblVerCount, $nudVerCount, $chkVerDate, $radVerBefore, $radVerAfter, $dtpVer))
|
||||
|
||||
# ── Scope ─────────────────────────────────────────────────────────────────────
|
||||
$grpVerScope = New-Group (T "grp.ver.scope") 10 118 620 76
|
||||
|
||||
$lblVerLib = New-Object System.Windows.Forms.Label
|
||||
$lblVerLib.Text = T "lbl.ver.library"
|
||||
$lblVerLib.Location = New-Object System.Drawing.Point(10, 22)
|
||||
$lblVerLib.Size = New-Object System.Drawing.Size(150, 20)
|
||||
|
||||
$txtVerLib = New-Object System.Windows.Forms.TextBox
|
||||
$txtVerLib.Location = New-Object System.Drawing.Point(164, 20)
|
||||
$txtVerLib.Size = New-Object System.Drawing.Size(230, 22)
|
||||
$txtVerLib.PlaceholderText = T "ph.ver.library"
|
||||
|
||||
$chkVerRecursive = New-Check (T "chk.ver.recursive") 10 48 260 $true
|
||||
$chkVerSubsites = New-Check (T "chk.ver.subsites") 280 48 200 $false
|
||||
|
||||
$grpVerScope.Controls.AddRange(@($lblVerLib, $txtVerLib, $chkVerRecursive, $chkVerSubsites))
|
||||
|
||||
# ── Options + Buttons ─────────────────────────────────────────────────────────
|
||||
$chkVerDryRun = New-Check (T "chk.ver.dryrun") 12 200 350 $true
|
||||
|
||||
$btnVerRun = New-ActionBtn (T "btn.ver.run") 10 228 ([System.Drawing.Color]::FromArgb(180, 60, 20))
|
||||
$btnVerRun.Size = New-Object System.Drawing.Size(180, 30)
|
||||
|
||||
$btnVerOpen = New-Object System.Windows.Forms.Button
|
||||
$btnVerOpen.Text = T "btn.ver.open"
|
||||
$btnVerOpen.Location = New-Object System.Drawing.Point(200, 228)
|
||||
$btnVerOpen.Size = New-Object System.Drawing.Size(130, 30)
|
||||
$btnVerOpen.Enabled = $false
|
||||
|
||||
$tabVersions.Controls.AddRange(@($grpVerKeep, $grpVerScope, $chkVerDryRun, $btnVerRun, $btnVerOpen))
|
||||
|
||||
$tabs.TabPages.AddRange(@($tabPerms, $tabStorage, $tabTemplates, $tabSearch, $tabDupes, $tabTransfer, $tabBulk, $tabStruct, $tabVersions))
|
||||
|
||||
# ── Progress bar ───────────────────────────────────────────────────────────────
|
||||
$progressBar = New-Object System.Windows.Forms.ProgressBar
|
||||
@@ -3864,6 +3954,19 @@ $_reg = {
|
||||
& $_reg $script:i18nMap $lblStructLib "lbl.struct.library"
|
||||
& $_reg $script:i18nMap $btnStructCreate "btn.struct.create"
|
||||
& $_reg $script:i18nMap $btnStructClear "btn.struct.clear"
|
||||
# Version Cleanup tab
|
||||
& $_reg $script:i18nMap $lblVerCount "lbl.ver.count"
|
||||
& $_reg $script:i18nMap $chkVerDate "chk.ver.date"
|
||||
& $_reg $script:i18nMap $radVerBefore "rad.ver.before"
|
||||
& $_reg $script:i18nMap $radVerAfter "rad.ver.after"
|
||||
& $_reg $script:i18nMap $lblVerLib "lbl.ver.library"
|
||||
& $_reg $script:i18nMap $chkVerRecursive "chk.ver.recursive"
|
||||
& $_reg $script:i18nMap $chkVerSubsites "chk.ver.subsites"
|
||||
& $_reg $script:i18nMap $chkVerDryRun "chk.ver.dryrun"
|
||||
& $_reg $script:i18nMap $btnVerRun "btn.ver.run"
|
||||
& $_reg $script:i18nMap $btnVerOpen "btn.ver.open"
|
||||
& $_reg $script:i18nMap $grpVerKeep "grp.ver.keep"
|
||||
& $_reg $script:i18nMap $grpVerScope "grp.ver.scope"
|
||||
|
||||
# Tab pages
|
||||
& $_reg $script:i18nTabs $tabPerms "tab.perms"
|
||||
@@ -3874,6 +3977,7 @@ $_reg = {
|
||||
& $_reg $script:i18nTabs $tabTransfer "tab.transfer"
|
||||
& $_reg $script:i18nTabs $tabBulk "tab.bulk"
|
||||
& $_reg $script:i18nTabs $tabStruct "tab.structure"
|
||||
& $_reg $script:i18nTabs $tabVersions "tab.versions"
|
||||
|
||||
# Menu items
|
||||
& $_reg $script:i18nMenus $menuSettings "menu.settings"
|
||||
@@ -3893,6 +3997,7 @@ $script:i18nPlaceholders = [System.Collections.Generic.Dictionary[string,object]
|
||||
& $_reg $script:i18nPlaceholders $txtXferSrcLib "ph.xfer.library"
|
||||
& $_reg $script:i18nPlaceholders $txtXferDstSite "ph.xfer.site"
|
||||
& $_reg $script:i18nPlaceholders $txtXferDstLib "ph.xfer.library"
|
||||
& $_reg $script:i18nPlaceholders $txtVerLib "ph.ver.library"
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -5768,6 +5873,199 @@ $btnStructCreate.Add_Click({
|
||||
}
|
||||
})
|
||||
|
||||
# ── Version Cleanup handlers ─────────────────────────────────────────────────
|
||||
$script:_VerReport = $null
|
||||
|
||||
$btnVerOpen.Add_Click({
|
||||
if ($script:_VerReport -and (Test-Path $script:_VerReport)) {
|
||||
Start-Process $script:_VerReport
|
||||
}
|
||||
})
|
||||
|
||||
$btnVerRun.Add_Click({
|
||||
# --- Gather all selected site URLs ---
|
||||
$siteUrls = @()
|
||||
if ($script:_CachedSites -and $script:_CachedSites.Count -gt 0) {
|
||||
foreach ($s in $script:_CachedSites) {
|
||||
if ($s.Checked) { $siteUrls += $s.Url }
|
||||
}
|
||||
}
|
||||
if ($siteUrls.Count -eq 0) {
|
||||
$single = $txtSiteUrl.Text.Trim()
|
||||
if ($single) { $siteUrls = @($single) }
|
||||
}
|
||||
if ($siteUrls.Count -eq 0) { Write-Log "Site URL required." "Red"; return }
|
||||
|
||||
$clientId = $txtClientId.Text.Trim()
|
||||
if (-not $clientId) { Write-Log "Client ID required." "Red"; return }
|
||||
|
||||
$keepCount = [int]$nudVerCount.Value
|
||||
$useDate = $chkVerDate.Checked
|
||||
$dateBefore = $radVerBefore.Checked # true = keep before, false = keep after
|
||||
$cutoffDate = $dtpVer.Value
|
||||
$library = $txtVerLib.Text.Trim()
|
||||
$recursive = $chkVerRecursive.Checked
|
||||
$subsites = $chkVerSubsites.Checked
|
||||
$dryRun = $chkVerDryRun.Checked
|
||||
|
||||
$btnVerRun.Enabled = $false
|
||||
Start-ProgressAnim
|
||||
$modeLabel = if ($dryRun) { "DRY RUN" } else { "LIVE" }
|
||||
Write-Log "=== VERSION CLEANUP ($modeLabel) ===" "White"
|
||||
Write-Log "Keep: $keepCount version(s)" "Gray"
|
||||
if ($useDate) {
|
||||
$dir = if ($dateBefore) { "before" } else { "after" }
|
||||
Write-Log "Date filter: keep versions $dir $($cutoffDate.ToString('yyyy-MM-dd'))" "Gray"
|
||||
}
|
||||
Write-Log ("-" * 52) "DarkGray"
|
||||
|
||||
$report = [System.Collections.Generic.List[object]]::new()
|
||||
$totalDeleted = 0
|
||||
$totalKept = 0
|
||||
$totalErrors = 0
|
||||
|
||||
try {
|
||||
foreach ($siteUrl in $siteUrls) {
|
||||
Write-Log "Connecting to $siteUrl ..." "Gray"
|
||||
Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $clientId
|
||||
|
||||
# Collect site URLs to process (main + subsites)
|
||||
$sitesToProcess = @($siteUrl)
|
||||
if ($subsites) {
|
||||
try {
|
||||
$subs = Get-PnPSubWeb -Recurse -ErrorAction SilentlyContinue
|
||||
foreach ($sw in $subs) { $sitesToProcess += $sw.Url }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
foreach ($currentSite in $sitesToProcess) {
|
||||
if ($currentSite -ne $siteUrl) {
|
||||
try { Connect-PnPOnline -Url $currentSite -Interactive -ClientId $clientId } catch {
|
||||
Write-Log " Cannot connect to subsite $currentSite — skipped" "DarkOrange"
|
||||
continue
|
||||
}
|
||||
}
|
||||
Write-Log "Processing site: $currentSite" "White"
|
||||
|
||||
# Get target lists
|
||||
$lists = @()
|
||||
if ($library) {
|
||||
try { $lists = @(Get-PnPList -Identity $library -ErrorAction Stop) } catch {
|
||||
Write-Log " Library '$library' not found — skipped" "DarkOrange"
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
$lists = Get-PnPList | Where-Object { $_.BaseTemplate -eq 101 -and $_.Hidden -eq $false }
|
||||
}
|
||||
|
||||
foreach ($list in $lists) {
|
||||
Write-Log " Library: $($list.Title)" "Gray"
|
||||
try {
|
||||
$camlQuery = "<View Scope='RecursiveAll'><Query></Query><RowLimit>5000</RowLimit></View>"
|
||||
if (-not $recursive) {
|
||||
$camlQuery = "<View><Query></Query><RowLimit>5000</RowLimit></View>"
|
||||
}
|
||||
$items = Get-PnPListItem -List $list.Title -Query $camlQuery -ErrorAction Stop |
|
||||
Where-Object { $_.FileSystemObjectType -eq "File" }
|
||||
} catch {
|
||||
Write-Log " Error listing files: $($_.Exception.Message)" "Red"
|
||||
$totalErrors++
|
||||
continue
|
||||
}
|
||||
|
||||
foreach ($item in $items) {
|
||||
try {
|
||||
$file = $item.FieldValues["FileRef"]
|
||||
$versions = Get-PnPFileVersion -Url $file -ErrorAction Stop
|
||||
|
||||
if ($versions.Count -le $keepCount) { continue }
|
||||
|
||||
# Sort versions oldest first (by VersionLabel numeric)
|
||||
$sorted = $versions | Sort-Object { [double]$_.VersionLabel }
|
||||
|
||||
# Determine which versions to delete
|
||||
$toDelete = @()
|
||||
foreach ($v in $sorted) {
|
||||
# Always keep the last $keepCount versions
|
||||
$idx = [array]::IndexOf($sorted, $v)
|
||||
$remaining = $sorted.Count - $idx
|
||||
if ($remaining -le $keepCount) { break }
|
||||
|
||||
# Apply date filter if enabled
|
||||
if ($useDate) {
|
||||
$vDate = [datetime]$v.Created
|
||||
if ($dateBefore) {
|
||||
# Keep versions before cutoff → delete versions ON or AFTER cutoff
|
||||
if ($vDate -lt $cutoffDate) { continue }
|
||||
} else {
|
||||
# Keep versions after cutoff → delete versions BEFORE cutoff
|
||||
if ($vDate -ge $cutoffDate) { continue }
|
||||
}
|
||||
}
|
||||
|
||||
$toDelete += $v
|
||||
}
|
||||
|
||||
if ($toDelete.Count -eq 0) { continue }
|
||||
|
||||
$fileName = Split-Path $file -Leaf
|
||||
foreach ($v in $toDelete) {
|
||||
if ($dryRun) {
|
||||
Write-Log " [DRY] Would delete v$($v.VersionLabel) of $fileName ($($v.Created))" "DarkOrange"
|
||||
} else {
|
||||
try {
|
||||
Remove-PnPFileVersion -Url $file -Identity $v.Id -Force -ErrorAction Stop
|
||||
Write-Log " Deleted v$($v.VersionLabel) of $fileName" "LightGreen"
|
||||
} catch {
|
||||
Write-Log " Error deleting v$($v.VersionLabel) of $fileName — $($_.Exception.Message)" "Red"
|
||||
$totalErrors++
|
||||
}
|
||||
}
|
||||
$totalDeleted++
|
||||
}
|
||||
|
||||
$kept = $sorted.Count - $toDelete.Count
|
||||
$totalKept += $kept
|
||||
|
||||
$report.Add([PSCustomObject]@{
|
||||
Site = $currentSite
|
||||
Library = $list.Title
|
||||
File = $file
|
||||
TotalVer = $sorted.Count
|
||||
Deleted = $toDelete.Count
|
||||
Kept = $kept
|
||||
})
|
||||
} catch {
|
||||
$totalErrors++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Export CSV report
|
||||
if ($report.Count -gt 0) {
|
||||
$outDir = $txtOutput.Text.Trim()
|
||||
if (-not $outDir) { $outDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } }
|
||||
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
|
||||
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$prefix = if ($dryRun) { "VersionCleanup_DryRun" } else { "VersionCleanup" }
|
||||
$csvFile = Join-Path $outDir "${prefix}_$stamp.csv"
|
||||
$report | Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8
|
||||
$script:_VerReport = $csvFile
|
||||
$btnVerOpen.Enabled = $true
|
||||
Write-Log "Report: $csvFile" "White"
|
||||
}
|
||||
|
||||
Write-Log "=== VERSION CLEANUP COMPLETE: $totalDeleted deleted, $totalKept kept, $totalErrors error(s) ===" "White"
|
||||
} catch {
|
||||
Write-Log "Error: $($_.Exception.Message)" "Red"
|
||||
} finally {
|
||||
$btnVerRun.Enabled = $true
|
||||
Stop-ProgressAnim
|
||||
}
|
||||
})
|
||||
|
||||
#endregion
|
||||
|
||||
# ── Initialisation : chargement des settings ───────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user