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" }