diff --git a/Sharepoint_ToolBox.ps1 b/Sharepoint_ToolBox.ps1
index 1f409d5..f5a13fc 100644
--- a/Sharepoint_ToolBox.ps1
+++ b/Sharepoint_ToolBox.ps1
@@ -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 = "5000"
+ if (-not $recursive) {
+ $camlQuery = "5000"
+ }
+ $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 ───────────────────────────────
diff --git a/lang/fr.json b/lang/fr.json
index c0347a6..222d3e5 100644
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -150,5 +150,20 @@
"btn.struct.create": "Créer l'arborescence",
"btn.struct.clear": "Effacer",
"struct.col.path": "Chemin complet",
- "struct.col.depth": "Profondeur"
+ "struct.col.depth": "Profondeur",
+
+ "tab.versions": " Versions ",
+ "grp.ver.keep": "Versions à conserver",
+ "lbl.ver.count": "Nombre de versions à garder :",
+ "chk.ver.date": "Filtrer aussi par date",
+ "rad.ver.before": "Garder les versions avant le :",
+ "rad.ver.after": "Garder les versions après le :",
+ "grp.ver.scope": "Périmètre",
+ "lbl.ver.library": "Bibliothèque / Dossier :",
+ "ph.ver.library": "Documents partagés",
+ "chk.ver.recursive": "Inclure les sous-dossiers (récursif)",
+ "chk.ver.subsites": "Inclure les sous-sites",
+ "chk.ver.dryrun": "Simulation (aperçu uniquement, aucune suppression)",
+ "btn.ver.run": "Nettoyer les versions",
+ "btn.ver.open": "Ouvrir le rapport"
}