
Ein Plan wird zwanzigmal überarbeitet, jede Fassung bleibt als Version erhalten. Bei einer 40-MB-Datei sind das 800 MB für eine einzige Zeichnung. In Projektablagen mit tausenden Plänen und Protokollen ist der Versionsverlauf schnell der größte Posten im SharePoint-Speicher, oft größer als die aktuellen Dateien selbst. Wer in den Speichermetriken einer Site nachsieht, findet ihn dort schwarz auf weiß.
Für einen Anlagenbauer mit sehr vielen Projekträumen haben wir das Löschen alter Versionen automatisiert. Dieser Beitrag zeigt, was SharePoint selbst dafür mitbringt, warum wir trotzdem eine eigene Lösung gebaut haben und wie sie im Kern funktioniert, mit Code.
Was SharePoint selbst mitbringt
Versionsgrenzen. Je Organisation, Site oder Bibliothek lässt sich festlegen, wie viele Hauptversionen erhalten bleiben oder nach wie vielen Tagen sie ablaufen. Mit der Einstellung „Automatisch“ dünnt SharePoint ältere Versionen selbst aus. Wichtig: Diese Grenzen gelten für neue Versionen. Der Bestand schrumpft erst, wenn eine Datei wieder bearbeitet wird. Dateien in abgeschlossenen Projekten werden nie mehr bearbeitet, ihr Verlauf bleibt.
Lösch-Jobs für den Bestand. Für vorhandene Versionen gibt es Batch-Jobs, die eine Site oder Bibliothek im Hintergrund bereinigen (New-SPOSiteFileVersionBatchDeleteJob, New-PnPSiteFileVersionBatchDeleteJob und die Varianten je Bibliothek). Sie laufen asynchron, werden je Site von einer Administratorin angestoßen, und die gelöschten Versionen sind laut Microsoft nicht über den Papierkorb wiederherstellbar.
Speichermetriken. Unter Websiteeinstellungen → Speichermetriken zeigt SharePoint je Ordner, wie viel Platz Versionen belegen. Ein guter Ausgangspunkt für die Frage, wo es sich lohnt.
Warum eine eigene Automatisierung
Für einzelne Sites reicht der Batch-Job. Bei sehr vielen Projekträumen fehlen drei Dinge:
- Die Fachabteilung entscheidet, nicht die IT. Welche Projekte bereinigt werden, weiß die Projektleitung. Sie soll es dort festlegen, wo sie ohnehin arbeitet: in der Projektliste.
- Ein Dienst ohne Benutzerkonto. Die Bereinigung läuft nachts als Azure Function mit Zertifikat. In unseren Tests lehnte der Batch-Lösch-Job ein solches App-only-Konto ab.
Remove-PnPFileVersionje Datei funktioniert dagegen app-only. - Nachvollziehbarkeit. Je Projekt soll stehen, wann was gelöscht wurde und wie viel Platz frei wurde, und am Ende soll eine Zusammenfassung in der Mailbox liegen.
Dazu kommt der Lebenszyklus: Die Versionsbereinigung ist der erste Schritt, bevor ein abgeschlossenes Projekt archiviert wird. Was ins Archiv geht, soll nicht zwanzig Fassungen jedes Plans mitschleppen.

Gesteuert über die Projektliste
Die Projektliste bekommt zwei Felder: eine Auswahl, ob die Versionen gelöscht werden sollen, und ein mehrzeiliges Protokoll.
# Steuerfeld und Protokoll in der Projektliste anlegen (PnP PowerShell)
Add-PnPField -List "Projektliste" -InternalName "DeleteVersions" -DisplayName "Versionen löschen" `
-Type Choice -Choices "No", "Yes", "Erledigt", "Fehler" -AddToDefaultView
Set-PnPField -List "Projektliste" -Identity "DeleteVersions" -Values @{ DefaultValue = "No"; Indexed = $true }
Add-PnPField -List "Projektliste" -InternalName "DeleteVersionsLog" -DisplayName "Versionen löschen – Protokoll" `
-Type Note
Die Projektleitung setzt „Yes“. Der Dienst verarbeitet nur diese Einträge und setzt danach „Erledigt“ oder „Fehler“. Wird ein Lauf durch das Zeitbudget unterbrochen, bleibt „Yes“ stehen, und der nächste Lauf setzt fort.
Die Kandidaten finden, auch in großen Listen
Eine Projektliste mit vielen tausend Einträgen liegt über dem Listenschwellenwert von 5.000. Eine Abfrage auf das Steuerfeld funktioniert dann nur mit Index, und einen Index nachträglich anzulegen, verweigert SharePoint bei solchen Listen. Deshalb gibt es einen Rückfallweg: seitenweise laden und im Skript filtern.
$caml = "<View><ViewFields><FieldRef Name='Title'/><FieldRef Name='URL'/><FieldRef Name='DeleteVersions'/></ViewFields>" +
"<Query><Where><Eq><FieldRef Name='DeleteVersions'/><Value Type='Choice'>Yes</Value></Eq></Where></Query></View>"
try {
$candidates = @(Get-PnPListItem -List "Projektliste" -Query $caml -Connection $root -ErrorAction Stop)
}
catch {
# Ohne Index scheitert die Abfrage am Listenschwellenwert: seitenweise laden, im Skript filtern
$candidates = @(Get-PnPListItem -List "Projektliste" -Fields "Title", "URL", "DeleteVersions" -PageSize 1000 -Connection $root |
Where-Object { [string]$_["DeleteVersions"] -eq "Yes" })
}
Der Kern: Versionen je Datei löschen
Je Projektraum geht der Dienst durch die Dokumentbibliotheken, ohne Systembibliotheken wie Websiteobjekte oder Formularvorlagen, und dort durch die Dateien. Dateien mit nur einer Version werden ohne weitere Abfrage übersprungen; das hält Folgeläufe billig.
function Remove-OldFileVersions {
param($Connection, $Library, [int]$KeepHistorical = 0, [datetime]$Deadline, [switch]$DryRun)
$deleted = 0; $bytes = [int64]0
$items = Get-PnPListItem -List $Library -Fields "FileRef", "FSObjType", "ContentVersion" -PageSize 500 -Connection $Connection
foreach ($item in $items) {
if ((Get-Date) -ge $Deadline) { return @{ Deleted = $deleted; Bytes = $bytes; TimedOut = $true } }
if ([string]$item["FSObjType"] -ne "0") { continue } # Ordner überspringen
if ([int]$item["ContentVersion"] -le 1) { continue } # keine Altversionen
$fileRef = [string]$item["FileRef"]
$versions = @(Get-PnPFileVersion -Url $fileRef -Connection $Connection) # alt nach neu
if ($versions.Count -le $KeepHistorical) { continue }
$toDelete = $versions | Select-Object -First ($versions.Count - $KeepHistorical)
$bytes += ($toDelete | Measure-Object -Property Size -Sum).Sum
if (-not $DryRun) {
if ($KeepHistorical -eq 0) {
Remove-PnPFileVersion -Url $fileRef -All -Force -Connection $Connection
} else {
$toDelete | ForEach-Object { Remove-PnPFileVersion -Url $fileRef -Identity $_.Id -Force -Connection $Connection }
}
}
$deleted += $toDelete.Count
}
return @{ Deleted = $deleted; Bytes = $bytes; TimedOut = $false }
}
Die aktuelle Version einer Datei taucht in Get-PnPFileVersion nicht auf, sie bleibt immer erhalten. KeepHistorical legt fest, wie viele ältere Fassungen zusätzlich bleiben; beim Anlagenbauer ist es keine.
Zeitbudget statt Abbruch
Eine Azure Function hat ein festes Zeitlimit. Statt mitten in einer Datei abgebrochen zu werden, rechnet der Dienst mit einem eigenen Budget, standardmäßig 150 Minuten bei einem Funktionslimit von drei Stunden. Ist es erreicht, schreibt er das Protokoll, lässt den Status auf „Yes“ und hört sauber auf. Weil bereits bereinigte Dateien nur noch eine Version haben, kostet der nächste Lauf für sie fast nichts.
{
"bindings": [
{ "name": "Timer", "type": "timerTrigger", "direction": "in", "schedule": "0 0 23 * * 5" }
]
}
Der Wochenlauf startet freitags um 23:00 UTC, ein zweiter Lauf für Gruppenseiten samstags früh, jeweils mit eigenem Zeitbudget.
Protokoll, Status und Zusammenfassung
Je Projekt entsteht ein Protokolleintrag mit Zeitpunkt, Bibliotheken, gelöschten Versionen und freigegebenem Speicher. Er wird zusammen mit dem Status in einem Schritt geschrieben:
$log = @"
=== $((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss'))Z - Wöchentlicher Lauf ===
Behalte: aktuelle Version + $KeepHistorical Altversion(en) pro Datei
OK 03 Pläne - Versionen gelöscht: 1284, frei: 2,1 GB
Status: Erledigt
"@
$previous = [string](Get-PnPListItem -List "Projektliste" -Id $itemId -Connection $root)["DeleteVersionsLog"]
Set-PnPListItem -List "Projektliste" -Identity $itemId -Connection $root -Values @{
DeleteVersions = $status # "Erledigt", "Fehler" oder unverändert "Yes"
DeleteVersionsLog = ($log + "`n`n" + $previous).Trim()
} | Out-Null
Nach jedem Lauf geht eine Zusammenfassung an die Fachabteilung: Projekt, freigegebener Speicher, Status. Scheitert der Lauf, kommt stattdessen eine Fehlermail.
Probelauf vor dem Ernstfall
Gelöschte Versionen sind weg. Deshalb gibt es neben dem Wochenlauf einen manuellen Aufruf über HTTP, der einzelne Einträge der Projektliste verarbeitet und mit dryRun nur rechnet, was frei würde:
{
"onlyListItemIds": [109],
"runLabel": "Probelauf Projekt 109",
"maxRunMinutes": 5,
"dryRun": true,
"skipLogWrite": true
}
Für die Frage vorab, wo sich die Bereinigung lohnt, gibt es zusätzlich ein Berichtsskript, das je Bibliothek die Größe der Altversionen in eine CSV-Datei schreibt.
Vorbeugen statt nur aufräumen
Manche Bibliotheken brauchen gar keine Versionen: Importbibliotheken, in die ein Dienst Bestellungen oder Tagesberichte als PDF ablegt, werden nie bearbeitet. Beim Anlegen neuer Projekträume schaltet das Provisioning die Versionierung dort ab. Was nicht entsteht, muss später niemand löschen.
Was Sie vorher klären sollten
- Aufbewahrung. Liegt eine Aufbewahrungsrichtlinie oder ein Legal Hold auf der Site, gehen Aufbewahrungspflichten vor. Klären Sie mit Compliance, welche Projekte bereinigt werden dürfen.
- Wie viele Fassungen bleiben. Nur die aktuelle oder zusätzlich die letzte Freigabe? Bei Plänen mit Index-Logik genügt oft die aktuelle Datei.
- Wer entscheidet. Projektleitung, Fachabteilung oder ein festes Kriterium wie „Projektphase Archiv“.
- Wann. Am Projektende als Schritt vor der Archivierung, oder regelmäßig für laufende Projekte.
Wie ein Projektraum seine Phasen vom Angebot bis zum Abschluss durchläuft, beschreibt der Beitrag Projektphasen in SharePoint. Den nächsten Schritt, das nachweisbare Archiv im eigenen Azure, übernimmt Smarter Archiving. Die Versionsbereinigung ist ein Baustein von Smarter Project Portal.
- Smarter Archiving
- SharePoint Online
- PowerShell
- PnP PowerShell
- Azure Functions
- Versionierung
- Speicherplatz