Public/Copy-sqmNTFSPermissions.ps1

<#
.SYNOPSIS
    Copies NTFS permissions (ACLs) from a source path to a destination path.
 
.DESCRIPTION
    Reads the explicit NTFS permissions for each file system object (folder/file) below
    the source path and applies them to the corresponding object below the destination path.
    The target structure must already exist (exception: with -CreateMissingFolders, missing
    target folders are created automatically).
 
.PARAMETER SourcePath
    Source path (e.g. "D:\" or "D:\Data").
 
.PARAMETER DestinationPath
    Destination path (e.g. "E:\" or "E:\Data").
 
.PARAMETER Recurse
    Recursive traversal of all subfolders and files.
 
.PARAMETER CreateMissingFolders
    Automatically creates missing target folders (directories only, not files).
    Files missing at the destination are skipped.
    Implies -Recurse: creating the missing folder structure only makes sense for the whole
    tree. Without it, only the top-level folder itself would be processed.
 
.PARAMETER IncludeSystemAndHidden
    Includes hidden and system objects in the processing.
 
.EXAMPLE
    Copy-sqmNTFSPermissions -SourcePath "D:\" -DestinationPath "E:\" -Recurse
    Copies all permissions from D: to E: (recursively).
 
.EXAMPLE
    Copy-sqmNTFSPermissions -SourcePath "D:\Daten" -DestinationPath "E:\Daten" -Recurse -CreateMissingFolders
    Copies permissions and creates missing target folders.
 
.NOTES
    Requires administrative rights (Get-Acl / Set-Acl).
    Access errors generate warnings; processing continues.
#>

function Copy-sqmNTFSPermissions {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [string]$SourcePath,

        [Parameter(Mandatory = $true, Position = 1)]
        [string]$DestinationPath,

        [switch]$Recurse,
        [switch]$CreateMissingFolders,
        [switch]$IncludeSystemAndHidden
    )

    begin {
        # Quellpfad auf den vollstaendigen Pfad normalisieren. Der relative Pfad jedes Objekts
        # wird per Substring aus FullName gebildet; ein relativ angegebener Quellpfad ("Daten")
        # ist kuerzer als FullName ("D:\Daten") und fuehrte dazu, dass die komplette
        # Verzeichniskette unter dem Ziel nachgebaut wurde.
        $SourcePath = (Get-Item -LiteralPath $SourcePath -Force).FullName.TrimEnd('\')
        $DestinationPath = [System.IO.Path]::GetFullPath($DestinationPath)

        # Fehlende Ordner anlegen heisst, die Struktur des ganzen Baums anlegen. Ohne -Recurse
        # wurde nur der oberste Ordner verarbeitet, und keiner der Unterordner entstand.
        if ($CreateMissingFolders -and -not $Recurse) {
            Write-Verbose "-CreateMissingFolders schliesst -Recurse ein: der gesamte Baum wird verarbeitet."
            $Recurse = [switch]$true
        }
        if (-not (Test-Path -LiteralPath $DestinationPath -PathType Container)) {
            if ($CreateMissingFolders) {
                if ($PSCmdlet.ShouldProcess($DestinationPath, "Erstelle Zielverzeichnis")) {
                    New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
                    Write-Verbose "Zielverzeichnis '$DestinationPath' wurde erstellt."
                }
            } else {
                throw "Zielpfad '$DestinationPath' existiert nicht. Verwenden Sie -CreateMissingFolders."
            }
        }

        if ($Recurse) {
            # -LiteralPath: mit -Path werden [ ] in Ordnernamen als Wildcard gelesen
            $allItems = @(Get-Item -LiteralPath $SourcePath -Force:$IncludeSystemAndHidden) +
                        @(Get-ChildItem -LiteralPath $SourcePath -Recurse -Force:$IncludeSystemAndHidden -ErrorAction SilentlyContinue)
        } else {
            $allItems = @(Get-Item -LiteralPath $SourcePath -Force:$IncludeSystemAndHidden)
        }

        $total   = $allItems.Count
        $current = 0
    }

    process {
        foreach ($sourceItem in $allItems) {
            $current++
            $relativePath = $sourceItem.FullName.Substring($SourcePath.Length).TrimStart('\')
            $destItemPath = if ($relativePath) { $DestinationPath.TrimEnd('\') + '\' + $relativePath } else { $DestinationPath }

            Write-Progress -Activity "Kopiere NTFS-Berechtigungen" `
                           -Status "$current von $total : $relativePath" `
                           -PercentComplete (($current / $total) * 100)

            if ($sourceItem.PSIsContainer) {
                if (-not (Test-Path -LiteralPath $destItemPath -PathType Container)) {
                    if ($CreateMissingFolders) {
                        if ($PSCmdlet.ShouldProcess($destItemPath, "Erstelle Ordner")) {
                            try {
                                New-Item -ItemType Directory -Path $destItemPath -Force -ErrorAction Stop | Out-Null
                                Write-Verbose "Ordner '$destItemPath' erstellt."
                            } catch {
                                Write-Warning "Fehler beim Erstellen von '$destItemPath': $($_.Exception.Message)"
                                continue
                            }
                        }
                    } else {
                        Write-Warning "Zielordner '$destItemPath' existiert nicht. Ueberspringe (-CreateMissingFolders verwenden)."
                        continue
                    }
                }
            } else {
                if (-not (Test-Path -LiteralPath $destItemPath -PathType Leaf)) {
                    Write-Warning "Zieldatei '$destItemPath' existiert nicht. Ueberspringe."
                    continue
                }
            }

            try {
                $acl = Get-Acl -LiteralPath $sourceItem.FullName -ErrorAction Stop
                if ($PSCmdlet.ShouldProcess($destItemPath, "Setze ACL von '$($sourceItem.FullName)'")) {
                    Set-Acl -LiteralPath $destItemPath -AclObject $acl -ErrorAction Stop
                    Write-Verbose "ACL kopiert: $relativePath"
                }
            } catch {
                Write-Warning "Fehler bei '$($sourceItem.FullName)' -> '$destItemPath': $($_.Exception.Message)"
            }
        }
        Write-Progress -Activity "Kopiere NTFS-Berechtigungen" -Completed
    }

    end {
        Write-Host "Abgeschlossen. $total Elemente verarbeitet." -ForegroundColor Green
    }
}