ScriptBib

PowerShell-Skript zur Archivierung alter Projektordner

Identifiziert automatisch archivierungswürdige Projekt- und Datenordner anhand des Alters der neuesten enthaltenen Datei. Das Skript ermöglicht eine sichere Voranalyse per DryRun, erstellt detaillierte CSV-Auswertungen und unterstützt sowohl das Kopieren als auch das Verschieben alter Datenbestände in ein Archiv. Besonders geeignet für Fileserver-Bereinigungen, Projektarchive und Datenmigrationen.
🆔 ID 0001 🏷 Version 1 📅 22.06.2026 00:21 👤 admin@scriptbib.local

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

  1. Passende Ordnertiefe bestimmen
  2. DryRun durchführen
  3. CSV-Ergebnisse prüfen
  4. Ausschlusspfade definieren
  5. Erneuten DryRun durchführen
  6. Optional Speicherplatz berechnen
  7. Archivziel prüfen
  8. Archivierung per Copy durchführen
  9. Archiv prüfen
  10. 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

⬇ Skript herunterladen
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"
}