PowerShell-Skript zur Archivierung alter Projektordner
Beschreibung
Zweck
Das Skript wurde entwickelt, um alte Projekt- und Datenbestände auf Datei- oder Netzlaufwerken automatisiert zu identifizieren und für eine Archivierung vorzubereiten.
Hierbei werden definierte Projektordner analysiert und anhand des Alters ihrer enthaltenen Dateien bewertet. Archivierungswürdige Ordner können anschließend entweder dokumentiert, kopiert oder verschoben werden.
Das Skript eignet sich insbesondere für:
- Projektlaufwerke
- Netzfreigaben
- Abteilungslaufwerke
- Historisch gewachsene Fileserver
- Datenmigrationen
- Speicherplatzbereinigungen
Funktionsweise
Das Skript arbeitet in mehreren Schritten:
|
Schritt |
Beschreibung |
|---|---|
|
1 |
Start im definierten Quellpfad |
|
2 |
Durchlaufen der Ordnerstruktur bis zur definierten Tiefe |
|
3 |
Bewertung aller Ordner auf dieser Ebene |
|
4 |
Rekursive Suche aller Dateien innerhalb des Prüfordners |
|
5 |
Ermittlung der neuesten Datei |
|
6 |
Vergleich mit dem definierten Stichtag |
|
7 |
Markierung als archivierungswürdig oder nicht archivierungswürdig |
|
8 |
Optionales Kopieren oder Verschieben |
|
9 |
Dokumentation der Ergebnisse in einer CSV-Datei |
Entscheidungslogik
Die Archivierungsentscheidung basiert ausschließlich auf dem Änderungsdatum der neuesten Datei innerhalb eines Ordners.
Ordner-Zeitstempel werden bewusst ignoriert.
Beispiel
Prüfordner:
X:\Test1\Test2\Test4\
Enthaltene Dateien:
- Konstruktion.txt → 29.01.2004
- Planung.pdf → 26.09.2005
Neueste Datei:
26.09.2005
Ergebnis:
Der Ordner wird archiviert.
Gegenbeispiel
Prüfordner:
X:\Test1\Test2\Test3\
Enthaltene Dateien:
- Konstruktion.txt → 29.01.2004
- Planung.pdf → 26.09.2025
Neueste Datei:
26.09.2025
Ergebnis:
Der Ordner wird nicht archiviert.
Warum werden Ordner nicht bewertet?
Ordner-Zeitstempel können sich ändern, obwohl keine Projektdaten bearbeitet wurden.
Beispiele:
- Ordner umbenannt
- Ordner verschoben
- Unterordner erstellt
- Berechtigungen geändert
Dadurch würden alte Projekte fälschlicherweise als aktiv erscheinen.
Deshalb gilt:
Die neueste Datei entscheidet.
Betriebsarten
Mode
|
Wert |
Beschreibung |
|---|---|
|
DryRun |
Nur Analyse, keine Datenänderung |
|
Copy |
Archivierungswürdige Ordner werden kopiert |
|
Move |
Archivierungswürdige Ordner werden verschoben |
Empfehlung
Produktive Läufe immer mit DryRun beginnen.
Anschließend zunächst Copy verwenden.
Move erst einsetzen, wenn die Ergebnisse geprüft wurden.
Archivierungsumfang
ArchiveContent
|
Wert |
Beschreibung |
|---|---|
|
StructureOnly |
Es wird nur die Ordnerstruktur erstellt |
|
WithFiles |
Ordner und Dateien werden archiviert |
Empfehlung
Für echte Archivierungen sollte grundsätzlich WithFiles verwendet werden.
StructureOnly eignet sich hauptsächlich für Tests oder zur Vorbereitung einer Zielstruktur.
Parameterübersicht
|
Parameter |
Beschreibung |
Standardwert |
|---|---|---|
|
SourcePath |
Quellpfad der Analyse |
- |
|
TargetPath |
Zielpfad für Archivierung |
- |
|
MaxDepth |
Zu prüfende Ordnertiefe |
3 |
|
OlderThanYears |
Maximales Alter der neuesten Datei |
5 |
|
Mode |
Betriebsart |
DryRun |
|
ArchiveContent |
Umfang der Archivierung |
WithFiles |
|
CalculateSize |
Berechnung des Speicherbedarfs |
Deaktiviert |
|
ExcludePaths |
Ausgeschlossene Pfade |
Leer |
|
CsvPath |
Speicherort der CSV-Datei |
.\ArchivAnalyse.csv |
Parameterdetails
|
Parameter |
Beschreibung |
Beispiel |
Hinweis |
|---|---|---|---|
|
SourcePath |
Startpunkt der Analyse |
-SourcePath “X:\Projekte” |
Pflichtparameter |
|
TargetPath |
Archivziel |
-TargetPath “Y:\Archiv” |
Nur bei Copy und Move erforderlich |
|
MaxDepth |
Prüftiefe der Ordnerstruktur |
-MaxDepth 3 |
Bestimmt die Archivierungseinheit |
|
OlderThanYears |
Altersgrenze in Jahren |
-OlderThanYears 5 |
Neueste Datei entscheidet |
|
Mode |
Betriebsart |
-Mode DryRun |
DryRun, Copy oder Move |
|
ArchiveContent |
Umfang der Archivierung |
-ArchiveContent WithFiles |
StructureOnly oder WithFiles |
|
CalculateSize |
Speicherplatz berechnen |
-CalculateSize |
Kann Laufzeit erhöhen |
|
ExcludePaths |
Pfade ausschließen |
-ExcludePaths “X:\Aktiv”,“X:\Test” |
Mehrere Pfade möglich |
|
CsvPath |
Speicherort der CSV-Ausgabe |
-CsvPath “C:\Temp\Analyse.csv” |
Für Dokumentation und Prüfung |
CSV-Ausgabe
Das Skript erzeugt eine CSV-Datei mit allen Analyseergebnissen.
Enthaltene Felder
|
Feld |
Beschreibung |
|---|---|
|
SourcePath |
Geprüfter Ordner |
|
TargetPath |
Zielpfad |
|
NewestFileDate |
Neueste Datei |
|
CutoffDate |
Berechneter Stichtag |
|
OlderThanLimit |
Archivierungswürdig Ja/Nein |
|
FileCount |
Anzahl Dateien |
|
SizeBytes |
Größe in Byte |
|
SizeGB |
Größe in Gigabyte |
|
Mode |
Verwendeter Modus |
|
Action |
Geplante Aktion |
Größenberechnung
Über den Parameter CalculateSize kann zusätzlich der benötigte Speicherplatz ermittelt werden.
Vorteile
- Kapazitätsplanung für Archivlaufwerke
- Abschätzung der Datenmenge
- Dokumentation des Archivvolumens
Hinweis
Die Berechnung kann bei großen Datenbeständen die Laufzeit deutlich erhöhen und wurde deshalb optional umgesetzt.
Ausschlusspfade
Über ExcludePaths können einzelne Bereiche vollständig von der Analyse ausgeschlossen werden.
Typische Anwendungsfälle
- Aktive Projekte
- Sonderprojekte
- Testbereiche
- Temporäre Daten
- Technische Verzeichnisse
Beispiel:
-ExcludePaths “X:\Projekte\Aktiv”,“X:\Projekte\Sonderprojekte”
Typische Aufrufe
Analyse
.\Archive-OldFolders.ps1 -SourcePath “X:\Projekte” -Mode DryRun
Analyse mit Größenberechnung
.\Archive-OldFolders.ps1 -SourcePath “X:\Projekte” -Mode DryRun -CalculateSize
Analyse mit Ausschlüssen
.\Archive-OldFolders.ps1 -SourcePath “X:\Projekte” -Mode DryRun -ExcludePaths “X:\Projekte\Aktiv”
Archivierung per Copy
.\Archive-OldFolders.ps1 -SourcePath “X:\Projekte” -TargetPath “Y:\Archiv” -Mode Copy
Archivierung per Move
.\Archive-OldFolders.ps1 -SourcePath “X:\Projekte” -TargetPath “Y:\Archiv” -Mode Move
Wichtige Hinweise
|
Thema |
Empfehlung |
|---|---|
|
Erste Ausführung |
Immer DryRun verwenden |
|
Prüftiefe |
MaxDepth vorab testen |
|
Archivierung |
Erst Copy, später Move |
|
Speicherbedarf |
Optional mit CalculateSize ermitteln |
|
Berechtigungen |
Lesezugriff auf Quelle und Schreibzugriff auf Ziel erforderlich |
|
Ausschlüsse |
Vor produktiver Nutzung prüfen |
|
CSV-Datei |
Vor Archivierung auswerten und dokumentieren |
Empfohlener Ablauf
- Passende Ordnertiefe bestimmen
- DryRun durchführen
- CSV-Ergebnisse prüfen
- Ausschlusspfade definieren
- Erneuten DryRun durchführen
- Optional Speicherplatz berechnen
- Archivziel prüfen
- Archivierung per Copy durchführen
- Archiv prüfen
- Optional per Move bereinigen
Fazit
Das Skript wurde entwickelt, um große Datenbestände strukturiert und nachvollziehbar zu archivieren. Die Bewertung erfolgt bewusst anhand der neuesten Datei eines Projektordners und nicht anhand von Ordner-Zeitstempeln. Durch DryRun, CSV-Auswertung, Ausschlusspfade und optionale Speicherplatzberechnung kann die Archivierung sicher vorbereitet und dokumentiert werden.
Skript
param(
[Parameter(Mandatory=$true)]
[string]$SourcePath,
[string]$TargetPath,
[int]$MaxDepth = 3,
[int]$OlderThanYears = 5,
[ValidateSet("DryRun", "Copy", "Move")]
[string]$Mode = "DryRun",
[ValidateSet("StructureOnly", "WithFiles")]
[string]$ArchiveContent = "WithFiles",
[switch]$CalculateSize,
[string[]]$ExcludePaths = @(),
[string]$CsvPath = ".\ArchivAnalyse.csv"
)
$CutoffDate = (Get-Date).AddYears(-$OlderThanYears)
if (($Mode -eq "Copy" -or $Mode -eq "Move") -and [string]::IsNullOrWhiteSpace($TargetPath)) {
throw "TargetPath muss bei Copy oder Move angegeben werden."
}
if (-not (Test-Path $SourcePath)) {
throw "SourcePath wurde nicht gefunden: $SourcePath"
}
function Test-IsExcludedPath {
param(
[string]$Path,
[string[]]$ExcludePaths
)
foreach ($ExcludePath in $ExcludePaths) {
$NormalizedExclude = $ExcludePath.TrimEnd("\")
$NormalizedPath = $Path.TrimEnd("\")
if ($NormalizedPath.Equals($NormalizedExclude, [System.StringComparison]::OrdinalIgnoreCase) -or
$NormalizedPath.StartsWith($NormalizedExclude + "\", [System.StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
function Get-FoldersAtExactDepth {
param(
[string]$Path,
[int]$CurrentDepth,
[int]$MaxDepth
)
Get-ChildItem -Path $Path -Directory -Force -ErrorAction SilentlyContinue | ForEach-Object {
if (-not (Test-IsExcludedPath -Path $_.FullName -ExcludePaths $ExcludePaths)) {
if ($CurrentDepth -eq $MaxDepth) {
$_
}
else {
Get-FoldersAtExactDepth -Path $_.FullName -CurrentDepth ($CurrentDepth + 1) -MaxDepth $MaxDepth
}
}
}
}
$Folders = Get-FoldersAtExactDepth -Path $SourcePath -CurrentDepth 1 -MaxDepth $MaxDepth
$Results = @()
foreach ($Folder in $Folders) {
if (Test-IsExcludedPath -Path $Folder.FullName -ExcludePaths $ExcludePaths) {
Write-Host "Überspringe ausgeschlossenen Pfad: $($Folder.FullName)"
continue
}
Write-Host "Prüfe: $($Folder.FullName)"
$Files = Get-ChildItem -Path $Folder.FullName -File -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object {
-not (Test-IsExcludedPath -Path $_.FullName -ExcludePaths $ExcludePaths)
}
if ($Files.Count -eq 0) {
$NewestDate = $null
$TotalBytes = $null
$IsOlder = $false
}
else {
$NewestDate = ($Files | Sort-Object LastWriteTime -Descending | Select-Object -First 1).LastWriteTime
$IsOlder = $NewestDate -lt $CutoffDate
if ($CalculateSize) {
$TotalBytes = ($Files | Measure-Object Length -Sum).Sum
}
else {
$TotalBytes = $null
}
}
$RelativePath = $Folder.FullName.Substring($SourcePath.Length).TrimStart("\")
$Destination = if ($TargetPath) { Join-Path $TargetPath $RelativePath } else { "" }
$Action = if ($IsOlder) { $Mode } else { "Skip" }
$Results += [PSCustomObject]@{
SourcePath = $Folder.FullName
TargetPath = $Destination
NewestFileDate = $NewestDate
CutoffDate = $CutoffDate
OlderThanLimit = $IsOlder
FileCount = $Files.Count
SizeBytes = $TotalBytes
SizeGB = if ($TotalBytes -ne $null) { [math]::Round($TotalBytes / 1GB, 2) } else { "" }
Mode = $Mode
ArchiveContent = $ArchiveContent
Action = $Action
}
if ($IsOlder -and $Mode -ne "DryRun") {
if ($ArchiveContent -eq "StructureOnly") {
New-Item -Path $Destination -ItemType Directory -Force | Out-Null
Get-ChildItem -Path $Folder.FullName -Directory -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object {
-not (Test-IsExcludedPath -Path $_.FullName -ExcludePaths $ExcludePaths)
} |
ForEach-Object {
$SubRelative = $_.FullName.Substring($Folder.FullName.Length).TrimStart("\")
$TargetDir = Join-Path $Destination $SubRelative
New-Item -Path $TargetDir -ItemType Directory -Force | Out-Null
}
}
if ($ArchiveContent -eq "WithFiles") {
if ($Mode -eq "Copy") {
robocopy $Folder.FullName $Destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:5
}
if ($Mode -eq "Move") {
robocopy $Folder.FullName $Destination /E /MOVE /COPY:DAT /DCOPY:DAT /R:2 /W:5
}
}
}
}
$Results | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 -Delimiter ";"
$ArchiveCandidates = $Results | Where-Object { $_.OlderThanLimit -eq $true }
Write-Host ""
Write-Host "Fertig."
Write-Host "CSV: $CsvPath"
Write-Host ""
Write-Host "Geprüfte Ordner: $($Results.Count)"
Write-Host "Archivierungswürdig: $($ArchiveCandidates.Count)"
Write-Host "Stichtag: $CutoffDate"
if ($CalculateSize) {
$TotalArchiveBytes = ($ArchiveCandidates | Measure-Object SizeBytes -Sum).Sum
Write-Host "Benötigter Speicher: $([math]::Round($TotalArchiveBytes / 1GB, 2)) GB"
}
else {
Write-Host "Benötigter Speicher: Nicht berechnet"
}