Public/Copy-FileEx.ps1

# .SYNOPSIS
# Copies files with advanced progress reporting and Windows API support.
#
# .DESCRIPTION
# Copy-FileEx provides enhanced file copy capabilities with detailed progress reporting.
# It leverages the Windows CopyFileEx API when available and falls back to managed file
# copy operations when necessary. Features include speed reporting, progress bars,
# recursive copying, and special character handling.
#
# .PARAMETER Path
# Path to source file(s) or directory. Supports wildcards.
#
# .PARAMETER LiteralPath
# Path to source file(s) or directory. Does not support wildcards. Use this when path contains special characters.
#
# .PARAMETER Destination
# Destination path where files will be copied to.
#
# .PARAMETER Include
# Optional array of include filters (e.g., "*.txt", "file?.doc").
#
# .PARAMETER Exclude
# Optional array of exclude filters (e.g., "*.tmp", "~*").
#
# .PARAMETER Recurse
# If specified, copies subdirectories recursively. Required for directory copies.
#
# .PARAMETER Force
# If specified, overwrites existing files. Without this, existing files are skipped.
#
# .PARAMETER PassThru
# If specified, returns objects representing copied items.
#
# .PARAMETER UseWinApi
# If true (default), uses Windows CopyFileEx API. If false, uses managed file copy.
#
# .EXAMPLE
# Copy-FileEx -Path "C:\source\file.txt" -Destination "D:\backup"
#
# Copies a single file with progress reporting.
#
# .EXAMPLE
# Copy-FileEx -Path "C:\source\folder" -Destination "D:\backup" -Recurse
#
# Copies a directory and all its contents recursively.
#
# .EXAMPLE
# Copy-FileEx -Path "C:\source\*.txt" -Destination "D:\backup" -Force
#
# Copies all .txt files, overwriting any existing files.
#
# .EXAMPLE
# Copy-FileEx -LiteralPath "C:\source\file[1].txt" -Destination "D:\backup"
#
# Copies a file with special characters in the name.
#
# .EXAMPLE
# Get-ChildItem "C:\source" -Filter "*.txt" | Copy-FileEx -Destination "D:\backup"
#
# Uses pipeline input for copying multiple files.
#
# .EXAMPLE
# Copy-FileEx -Path "C:\source\large.iso" -Destination "D:\backup" -UseWinApi $false
#
# Forces use of managed copy method instead of Windows API.
#
# .NOTES
# Author: LordBubbles
# Module: PSCopyFileEx
# Version: 1.0.4
#
# Performance Notes:
# - Windows API method is generally faster
# - Large files benefit from API buffering
# - Network paths use compressed traffic when possible
#
# .LINK
# https://github.com/LordBubblesDev/PSCopyFileEx

Function Copy-FileEx {
    [CmdletBinding(SupportsShouldProcess=$true, DefaultParameterSetName='Path')]
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName='Path')]
        [string[]]$Path,

        [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true, ParameterSetName='LiteralPath')]
        [Alias('LP')]
        [string[]]$LiteralPath,

        [Parameter(Position=1, ValueFromPipelineByPropertyName=$true)]
        [string]$Destination,

        [Parameter()]
        [string[]]$Include,

        [Parameter()]
        [string[]]$Exclude,

        [Parameter()]
        [switch]$Recurse,

        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [switch]$PassThru,

        [Parameter()]
        [bool]$UseWinApi = $true
    )

    begin {
        Write-Debug @"
`n=== Copy-FileEx Debug Information ===
Parameter Set: $($PSCmdlet.ParameterSetName)
Path: $($Path -join ', ')
LiteralPath: $($LiteralPath -join ', ')
Destination: $Destination
Include: $($Include -join ', ')
Exclude: $($Exclude -join ', ')
Recurse: $Recurse
Force: $Force
UseWinApi: $UseWinApi
=================================
"@


        # Generate a random progress ID to avoid conflicts
        $progressId = Get-Random -Minimum 0 -Maximum 1000
        $childProgressId = $progressId + 1

        # Initialize cancellation support and register CTRL+C handler
        $script:cancelRequested = $false
        $null = [Console]::TreatControlCAsInput = $true
        
        # Function to check for CTRL+C
        function Test-CancellationRequested {
            if ([Console]::KeyAvailable) {
                $key = [Console]::ReadKey($true)
                if ($key.Key -eq 'C' -and $key.Modifiers -eq 'Control') {
                    Write-Warning "Cancellation requested by user"
                    $script:cancelRequested = $true
                    return $true
                }
            }
            return $false
        }

        # Initialize all variables that will be used across the function
        $script:speedSampleSize = 100  # Number of samples to average
        $script:speedSamples = @()
        $script:lastSpeedCheck = [DateTime]::Now
        $script:lastBytesForSpeed = 0
        $script:lastTime = [DateTime]::Now
        $script:lastBytes = 0
        $script:lastSpeedUpdate = [DateTime]::Now
        $script:currentSpeed = 0
        $script:lastProgressUpdate = [DateTime]::Now
        $script:progressThreshold = [TimeSpan]::FromMilliseconds(100)

        function Format-FileSize {
            param([long]$Size)
            
            switch ($Size) {
                { $_ -gt 1TB } { "{0:n2} TB" -f ($_ / 1TB); Break }
                { $_ -gt 1GB } { "{0:n2} GB" -f ($_ / 1GB); Break }
                { $_ -gt 1MB } { "{0:n2} MB" -f ($_ / 1MB); Break }
                { $_ -gt 1KB } { "{0:n2} KB" -f ($_ / 1KB); Break }
                default { "{0} B " -f $_ }
            }
        }
    
        function Get-CurrentSpeed {
            param (
                [DateTime]$now,
                [long]$currentBytes
            )
            
            $timeDiff = ($now - $lastSpeedCheck).TotalSeconds
            if ($timeDiff -gt 0) {
                $bytesDiff = $currentBytes - $lastBytesForSpeed
                $speed = $bytesDiff / $timeDiff
                
                # Add to rolling samples
                $speedSamples += $speed
                if ($speedSamples.Count -gt $speedSampleSize) {
                    $speedSamples = $speedSamples | Select-Object -Last $speedSampleSize
                }
                
                # Calculate average speed
                $avgSpeed = ($speedSamples | Measure-Object -Average).Average
                
                # Update last values
                $script:lastSpeedCheck = $now
                $script:lastBytesForSpeed = $currentBytes
                
                return $avgSpeed
            }
            return 0
        }

        # Attempt to use Windows API for CopyFileEx if UseWinApi is true
        if ($UseWinApi) {
            $useWin32Api = $true
            
            # Check if type already exists
            if (-not ([System.Management.Automation.PSTypeName]'Win32Helpers.Win32CopyFileEx').Type) {
                $signature = @'
                [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
                public static extern bool CopyFileEx(
                    string lpExistingFileName,
                    string lpNewFileName,
                    CopyProgressRoutine lpProgressRoutine,
                    IntPtr lpData,
                    ref bool pbCancel,
                    uint dwCopyFlags
                );

                public delegate uint CopyProgressRoutine(
                    long TotalFileSize,
                    long TotalBytesTransferred,
                    long StreamSize,
                    long StreamBytesTransferred,
                    uint dwStreamNumber,
                    uint dwCallbackReason,
                    IntPtr hSourceFile,
                    IntPtr hDestinationFile,
                    IntPtr lpData
                );
'@

                try {
                    Add-Type -MemberDefinition $signature -Name "Win32CopyFileEx" -Namespace "Win32Helpers"
                }
                catch {
                    Write-Warning "Failed to detect the Win32 API library."
                    $useWin32Api = $false
                }
            }
        }
        if ($useWin32Api) {
            Write-Verbose "Using Windows API for file copy operations"
        } else {
            Write-Verbose "Using managed file copy method"
        }
    }

    process {
        $filesCopied = 0
        $totalCopiedSize = 0

        # Add cancellation check at the start of process block
        if ($script:cancelCheckJob.State -eq 'Completed' -and $script:cancelCheckJob.Output) {
            $script:cancelRequested = $true
            Write-Warning "Operation cancelled by user"
            return
        }

        Write-Debug "Process block started"
        # Handle both Path and LiteralPath parameters
        $pathsToProcess = @()
        if ($LiteralPath) {
            Write-Debug "Using LiteralPath: $($LiteralPath -join ', ')"
            $pathsToProcess += $LiteralPath
            $useWildcards = $false
        } else {
            Write-Debug "Using Path: $($Path -join ', ')"
            $pathsToProcess += $Path
            $useWildcards = $true
        }

        foreach ($currentPath in $pathsToProcess) {
            Write-Debug "Processing path: $currentPath"
            try {
                Write-Debug "Testing path existence"
                if (Test-Path -LiteralPath $currentPath) {
                    Write-Debug "Path exists (LiteralPath): $currentPath"
                    $resolvedPaths = @([pscustomobject]@{
                        Path = $currentPath
                        ProviderPath = (Get-Item -LiteralPath $currentPath).FullName
                    })
                    Write-Debug "Resolved to: $($resolvedPaths.ProviderPath)"
                } else {
                    Write-Debug "Path does not exist directly, attempting resolution"
                    if ($useWildcards) {
                        Write-Debug "Resolving with wildcards"
                        $resolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop
                    } else {
                        Write-Debug "Resolving without wildcards"
                        $resolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop
                    }
                    Write-Debug "Resolved paths count: $($resolvedPaths.Count)"
                }

                foreach ($resolvedPath in $resolvedPaths) {
                    Write-Debug "Processing resolved path: $($resolvedPath.Path)"
                    
                    # Check if the current path is a directory
                    $isDirectory = (Get-Item -LiteralPath $resolvedPath.Path) -is [System.IO.DirectoryInfo]
                    Write-Debug "Is Directory: $isDirectory"
                    
                    $shouldProcess = $true
                    if (-not $isDirectory) {
                        if ($Include) {
                            Write-Debug "Checking Include filters: $($Include -join ', ')"
                            $shouldProcess = $resolvedPath.Path | Where-Object { 
                                $item = $_
                                $matchResult = ($Include | ForEach-Object { $item -like $_ }) -contains $true
                                Write-Debug "Include match result for $item : $matchResult"
                                return $matchResult
                            }
                        }
                        if ($Exclude -and $shouldProcess) {
                            Write-Debug "Checking Exclude filters: $($Exclude -join ', ')"
                            $shouldProcess = $resolvedPath.Path | Where-Object { 
                                $item = $_
                                $matchResult = ($Exclude | ForEach-Object { $item -like $_ }) -notcontains $true
                                Write-Debug "Exclude match result for $item : $matchResult"
                                return $matchResult
                            }
                        }
                    }

                    Write-Debug "Should process path: $shouldProcess"
                    if ($shouldProcess) {
                        $targetPath = $Destination
                        Write-Debug "Target path: $targetPath"
                        if ($Force -or $PSCmdlet.ShouldProcess($targetPath)) {
                            # Handle wildcards in path
                            $sourcePath = Split-Path -Path $currentPath -Parent
                            $sourceFilter = Split-Path -Path $currentPath -Leaf

                            try {
                                # Initialize variables
                                $isFile = $false
                                $relativePath = $null
                                
                                if ($sourceFilter.Contains('*')) {
                                    Write-Debug "Path contains wildcards: $sourceFilter"
                                    # Path contains wildcards
                                    Write-Debug "Getting child items with filter: $sourceFilter"
                                    $files = Get-ChildItem -Path $sourcePath -Filter $sourceFilter -File -Recurse:$Recurse -ErrorAction Stop
                                    Write-Debug "Found $($files.Count) files matching filter"
                                    $isFile = $false
                                    $basePath = $sourcePath
                                } else {
                                    Write-Debug "Path is direct: $currentPath"
                                    # Single file or directory
                                    $item = Get-Item -LiteralPath $currentPath -ErrorAction Stop
                                    $isFile = $item -is [System.IO.FileInfo]
                                    Write-Debug "Item is file: $isFile"
                                    if ($isFile) {
                                        Write-Debug "Single file: $($item.Name)"
                                        $files = @($item)
                                        $basePath = Split-Path -Path $item.FullName -Parent
                                        # For single files, use the file's directory as base path
                                        $relativePath = $item.Name
                                    } else {
                                        Write-Debug "Directory: $($item.FullName)"
                                        # For directories, apply Include/Exclude filters to Get-ChildItem
                                        $gciParams = @{
                                            Path = $currentPath
                                            File = $true
                                            Recurse = $Recurse
                                            ErrorAction = 'Stop'
                                        }
                                        if ($Include) {
                                            $gciParams['Include'] = $Include
                                            Write-Debug "Adding Include filter to Get-ChildItem: $($Include -join ', ')"
                                        }
                                        if ($Exclude) {
                                            $gciParams['Exclude'] = $Exclude
                                            Write-Debug "Adding Exclude filter to Get-ChildItem: $($Exclude -join ', ')"
                                        }
                                        Write-Debug "Getting child items with parameters: $($gciParams | ConvertTo-Json)"
                                        $files = Get-ChildItem @gciParams
                                        Write-Debug "Found $($files.Count) files in directory"
                                        $basePath = $item.FullName
                                    }
                                }
                                Write-Debug "Base Path: $basePath"
                            }
                            catch {
                                Write-Warning "Error accessing path: $_"
                                continue
                            }

                            if ($files.Count -eq 0) {
                                Write-Warning "No files found to copy"
                                continue
                            }

                            try {
                                $totalSize = ($files | Measure-Object -Property Length -Sum).Sum
                                Write-Verbose "Total size: $(Format-FileSize $totalSize)"
                                Write-Verbose "Total files: $($files.Count)"
                            }
                            catch {
                                Write-Warning "Error calculating size: $_"
                                continue
                            }

                            $totalBytesCopied = 0
                            $startTime = [DateTime]::Now

                            # Initialize variables for managed copy method
                            if (-not $useWin32Api) {
                                # Set up buffer and timing
                                $bufferSize = 4MB
                                $buffer = New-Object byte[] $bufferSize
                                
                                # Reset speed calculation variables for new copy operation
                                $script:speedSamples = @()
                                $script:lastSpeedCheck = $startTime
                                $script:lastBytesForSpeed = 0
                                $script:lastProgressUpdate = $startTime
                            }

                            # Show initial progress only for multiple files
                            if ($files.Count -gt 1) {
                                Write-Progress -Activity "Copying files" `
                                    -Status "0 of $($files.Count) files (0 of $(Format-FileSize $totalSize))" `
                                    -PercentComplete 0 `
                                    -Id $progressId
                            }

                            $filesCopied = 0
                            $verboseOutput = @()  # Collect verbose messages
                            foreach ($file in $files) {
                                $failed = $false
                                $skipped = $false
                                # Calculate relative path for destination
                                if ($isFile) {
                                    # Use pre-calculated relative path for single files
                                    $destPath = Join-Path $Destination $relativePath
                                } else {
                                    # Calculate relative path for files in directories
                                    $relativePath = $file.FullName.Substring($basePath.Length).TrimStart('\')
                                    $destPath = Join-Path $Destination $relativePath
                                }

                                # Create destination directory if it doesn't exist
                                $destDir = Split-Path -Path $destPath -Parent
                                if (-not (Test-Path -Path $destDir)) {
                                    New-Item -Path $destDir -ItemType Directory -Force | Out-Null
                                }

                                # Check if destination file exists and handle Force parameter
                                if (Test-Path -LiteralPath $destPath) {
                                    if (-not $Force) {
                                        Write-Warning "Destination file already exists: $destPath. Use -Force to overwrite."
                                        $skipped = $true
                                        continue
                                    }
                                    Write-Verbose "Overwriting existing file: $destPath"
                                }

                                try {
                                    if ($useWin32Api) {
                                        $cancel = $false
                                        
                                        # Determine optimal copy flags
                                        $copyFlags = 0
                                        if ($file.Length -gt 10MB) {
                                            $copyFlags = $copyFlags -bor 0x00001000  # COPY_FILE_NO_BUFFERING
                                        }
                                        
                                        # Check if path is network path or mapped drive more safely
                                        if ($destPath -like "\\*") {
                                            $copyFlags = $copyFlags -bor 0x10000000  # COPY_FILE_REQUEST_COMPRESSED_TRAFFIC
                                        }
                                        elseif ($destPath -match "^[A-Z]:\\") {
                                            try {
                                                $drive = Get-PSDrive -Name $destPath[0] -ErrorAction Stop
                                                if ($drive.DisplayRoot -like "\\*") {
                                                    $copyFlags = $copyFlags -bor 0x10000000  # COPY_FILE_REQUEST_COMPRESSED_TRAFFIC
                                                }
                                            }
                                            catch {
                                                Write-Debug "Drive check failed: $_"
                                            }
                                        }

                                        # Create script-scope variables for the callback
                                        $script:currentFile = $file
                                        $script:filesCount = $files.Count
                                        $script:filesCopied = $filesCopied
                                        $script:totalBytesCopied = $totalBytesCopied
                                        $script:totalSize = $totalSize
                                        $script:progressId = $progressId
                                        $script:lastSpeedUpdate = [DateTime]::Now
                                        $script:lastTime = [DateTime]::Now
                                        $script:lastBytes = 0
                                        $script:currentSpeed = 0

                                        $callback = {
                                            param(
                                                [long]$TotalFileSize,
                                                [long]$TotalBytesTransferred,
                                                [long]$StreamSize,
                                                [long]$StreamBytesTransferred,
                                                [uint32]$StreamNumber,
                                                [uint32]$CallbackReason,
                                                [IntPtr]$SourceFile,
                                                [IntPtr]$DestinationFile,
                                                [IntPtr]$Data
                                            )
                                            
                                            try {
                                                # Check for cancellation
                                                if (Test-CancellationRequested) {
                                                    Write-Warning "Cancellation requested by user"
                                                    return [uint32]1  # PROGRESS_CANCEL
                                                }

                                                # Use API values directly
                                                $percent = [math]::Min([math]::Round(($TotalBytesTransferred * 100) / [math]::Max($TotalFileSize, 1), 0), 100)

                                                # Update speed once per second
                                                $now = [DateTime]::Now
                                                if (($now - $script:lastSpeedUpdate).TotalSeconds -ge 1) {
                                                    $timeDiff = ($now - $script:lastTime).TotalSeconds
                                                    if ($timeDiff -gt 0) {
                                                        $bytesDiff = $TotalBytesTransferred - $script:lastBytes
                                                        $script:currentSpeed = $bytesDiff / $timeDiff
                                                        $script:lastTime = $now
                                                        $script:lastBytes = $TotalBytesTransferred
                                                        $script:lastSpeedUpdate = $now
                                                    }
                                                }

                                                if ($script:filesCount -gt 1) {
                                                    # Overall progress
                                                    $totalPercent = [math]::Min([math]::Round((($script:totalBytesCopied + $TotalBytesTransferred) / $script:totalSize * 100), 0), 100)
                                                    $totalCopiedAll = [math]::Min([math]::Round((($script:totalBytesCopied + $TotalBytesTransferred)), 0), $script:totalSize)
                                                    
                                                    Write-Progress -Activity "Copying files ($totalPercent%)" `
                                                        -Status "$($script:filesCopied) of $($script:filesCount) files ($(Format-FileSize $totalCopiedAll) of $(Format-FileSize $script:totalSize)) - $(Format-FileSize $script:currentSpeed)/s" `
                                                        -PercentComplete $totalPercent `
                                                        -Id $script:progressId

                                                    # File progress
                                                    Write-Progress -Activity "Copying $($script:currentFile.Name) ($percent%)" `
                                                        -Status "$(Format-FileSize $TotalBytesTransferred) of $(Format-FileSize $TotalFileSize)" `
                                                        -PercentComplete $percent `
                                                        -ParentId $script:progressId `
                                                        -Id ($script:progressId + 1)
                                                } else {
                                                    Write-Progress -Activity "Copying $($script:currentFile.Name) ($percent%)" `
                                                        -Status "$(Format-FileSize $TotalBytesTransferred) of $(Format-FileSize $TotalFileSize) - $(Format-FileSize $script:currentSpeed)/s" `
                                                        -PercentComplete $percent `
                                                        -Id $script:progressId
                                                }
                                            }
                                            catch {
                                                Write-Warning "Progress callback error: $_"
                                                return [uint32]3  # PROGRESS_QUIET
                                            }
                                            
                                            return [uint32]0  # PROGRESS_CONTINUE
                                        }

                                        $result = [Win32Helpers.Win32CopyFileEx]::CopyFileEx(
                                            $file.FullName,
                                            $destPath,
                                            $callback,
                                            [IntPtr]::Zero,
                                            [ref]$cancel,
                                            $copyFlags
                                        )

                                        if (-not $result) {
                                            $errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
                                            $failed = $true
                                            if ($script:cancelRequested) {
                                                Write-Warning "File copy cancelled by user"
                                                break
                                            }
                                            throw "CopyFileEx failed with error code: $errorCode"
                                        }

                                        $totalBytesCopied += $file.Length
                                    }
                                    else {
                                        $continueProcessing = $true
                                        $sourceStream = $null
                                        $destStream = $null
                                        
                                        try {
                                            $sourceStream = [System.IO.FileStream]::new($file.FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
                                            $destStream = [System.IO.FileStream]::new($destPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)
                                            $bytesRead = 0
                                            $fileSize = [Math]::Max($file.Length, 1)
                                            $fileBytesCopied = 0
                                
                                            :copyLoop while ($continueProcessing -and -not $script:cancelRequested -and ($bytesRead = $sourceStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
                                                # Check for cancellation before writing
                                                if (Test-CancellationRequested) {
                                                    Write-Warning "Cancelling file copy operation..."
                                                    $continueProcessing = $false
                                                    $failed = $true
                                                    break copyLoop
                                                }

                                                try {
                                                    $destStream.Write($buffer, 0, $bytesRead)
                                                }
                                                catch {
                                                    Write-Warning "Error writing to destination: $_"
                                                    $continueProcessing = $false
                                                    $failed = $true
                                                    break copyLoop
                                                }
                                                
                                                $fileBytesCopied += $bytesRead
                                                $totalBytesCopied += $bytesRead
                                
                                                # Update progress less frequently
                                                $now = [DateTime]::Now
                                                if (($now - $lastProgressUpdate) -gt $progressThreshold) {
                                                    if ($script:cancelRequested) {
                                                        $continueProcessing = $false
                                                        $failed = $true
                                                        break copyLoop
                                                    }

                                                    $totalPercent = [math]::Min([math]::Round(($totalBytesCopied / $totalSize * 100), 0), 100)
                                                    $filePercent = [math]::Min([math]::Round(($fileBytesCopied / $fileSize * 100), 0), 100)
                                                    
                                                    # Calculate current speed
                                                    $currentSpeed = Get-CurrentSpeed -now $now -currentBytes $totalBytesCopied
                                                    $speedText = if ($currentSpeed -gt 0) {
                                                        "$(Format-FileSize $currentSpeed)/s"
                                                    } else {
                                                        "0 B/s"
                                                    }
                                
                                                    if ($files.Count -gt 1) {
                                                        # Overall progress for multiple files
                                                        Write-Progress -Activity "Copying files ($totalPercent%)" `
                                                            -Status "$filesCopied of $($files.Count) files ($(Format-FileSize $totalBytesCopied) of $(Format-FileSize $totalSize)) - $speedText" `
                                                            -PercentComplete $totalPercent `
                                                            -Id $progressId
                                
                                                        # File progress as child
                                                        Write-Progress -Activity "Copying $($file.Name) ($filePercent%)" `
                                                            -Status "$(Format-FileSize $fileBytesCopied) of $(Format-FileSize $fileSize)" `
                                                            -PercentComplete $filePercent `
                                                            -ParentId $progressId `
                                                            -Id $childProgressId
                                                    } else {
                                                        # Single file progress
                                                        Write-Progress -Activity "Copying $($file.Name) ($filePercent%)" `
                                                            -Status "$(Format-FileSize $fileBytesCopied) of $(Format-FileSize $fileSize) - $speedText" `
                                                            -PercentComplete $filePercent `
                                                            -Id $progressId
                                                    }
                                
                                                    $lastProgressUpdate = $now
                                                }
                                            }
                                        }
                                        catch {
                                            Write-Warning "Error during file copy: $_"
                                            $continueProcessing = $false
                                            $failed = $true
                                        }
                                        finally {
                                            # Ensure streams are closed and disposed immediately
                                            if ($sourceStream) {
                                                try {
                                                    $sourceStream.Close()
                                                    $sourceStream.Dispose()
                                                }
                                                catch { }
                                                $sourceStream = $null
                                            }
                                            if ($destStream) {
                                                try {
                                                    $destStream.Close()
                                                    $destStream.Dispose()
                                                }
                                                catch { }
                                                $destStream = $null
                                            }

                                            # Clean up partial file if cancelled or failed
                                            if ((-not $continueProcessing) -or $script:cancelRequested -or $failed) {
                                                Write-Verbose "Cleaning up partial file after cancellation/failure: $destPath"
                                                try {
                                                    # Close any remaining handles
                                                    [System.GC]::Collect()
                                                    [System.GC]::WaitForPendingFinalizers()
                                                    
                                                    if (Test-Path -LiteralPath $destPath) {
                                                        # Force close any open handles
                                                        try {
                                                            $null = [System.IO.FileInfo]::new($destPath).Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None).Close()
                                                        } catch {
                                                            Write-Verbose "File is still locked, attempting cleanup anyway"
                                                        }
                                                        
                                                        # Try to delete the file multiple times with increasing delays
                                                        $maxRetries = 30  # Increased retries
                                                        $retryCount = 0
                                                        $deleted = $false
                                                        
                                                        while (-not $deleted -and $retryCount -lt $maxRetries) {
                                                            try {
                                                                # Try to take ownership and set full permissions
                                                                $acl = Get-Acl -LiteralPath $destPath
                                                                $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
                                                                $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity.Name,"FullControl","Allow")
                                                                $acl.SetAccessRule($rule)
                                                                Set-Acl -LiteralPath $destPath -AclObject $acl -ErrorAction SilentlyContinue
                                                                
                                                                # Force remove read-only if set
                                                                Set-ItemProperty -LiteralPath $destPath -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue
                                                                
                                                                # Try to delete
                                                                Remove-Item -LiteralPath $destPath -Force -ErrorAction Stop
                                                                if (-not (Test-Path -LiteralPath $destPath)) {
                                                                    $deleted = $true
                                                                    Write-Verbose "Successfully removed partial file: $destPath"
                                                                    break
                                                                }
                                                            }
                                                            catch {
                                                                $retryCount++
                                                                if ($retryCount -lt $maxRetries) {
                                                                    Write-Warning ("Retry {0} of {1}: Failed to remove partial file, retrying in {2} seconds..." -f $retryCount, $maxRetries, [math]::Min(2 * $retryCount, 10))
                                                                    Start-Sleep -Seconds ([math]::Min(2 * $retryCount, 10))
                                                                }
                                                                else {
                                                                    Write-Warning ("Failed to remove partial file after {0} attempts: {1}" -f $maxRetries, $destPath)
                                                                    Write-Warning $_.Exception.Message
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                                catch {
                                                    Write-Warning ("Failed to clean up partial file {0}: {1}" -f $destPath, $_.Exception.Message)
                                                }
                                            }
                                        }
                                        
                                        # Exit immediately if cancelled
                                        if ($script:cancelRequested) {
                                            return
                                        }
                                    }
                                }
                                catch {
                                    Write-Warning "Error copying $($file.Name): $_"
                                    $failed = $true
                                    if ($script:cancelRequested) {
                                        break
                                    }
                                }
                                finally {
                                    if (-not $failed -and -not $skipped) {
                                        $verboseOutput += "Copied '$($file.Name)' to '$destPath'"
                                        $filesCopied++
                                        $totalCopiedSize += $file.Length
                                    }
                                }

                                # Break the file loop if cancellation was requested
                                if ($script:cancelRequested) {
                                    break
                                }
                            }

                            # Calculate total elapsed time
                            $endTime = [DateTime]::Now
                            $elapsedTime = $endTime - $startTime
                            $elapsedText = if ($elapsedTime.TotalHours -ge 1) {
                                "{0:h'h 'm'm 's's'}" -f $elapsedTime
                            } elseif ($elapsedTime.TotalMinutes -ge 1) {
                                "{0:m'm 's's'}" -f $elapsedTime
                            } else {
                                "{0:s's'}" -f $elapsedTime
                            }

                            # Only show completion messages if not cancelled
                            if (-not $script:cancelRequested) {
                                if ($files.Count -gt 1) {
                                    $verboseOutput += "Total copied: $(Format-FileSize $totalCopiedSize) ($($filesCopied) files)"
                                } else {
                                    $verboseOutput += "Total copied: $(Format-FileSize $totalCopiedSize)"
                                }

                                $verboseOutput += "Operation completed in $elapsedText"
                            }

                            # Write all verbose messages at once after copying is complete
                            if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) {
                                if ($files.Count -gt 1) {
                                    $verboseOutput | ForEach-Object { Write-Verbose $_ }
                                } else {
                                    $verboseOutput | ForEach-Object { Write-Output $_ }
                                }
                            }

                            # Complete progress bars
                            if ($files.Count -gt 1) {
                                Write-Progress -Activity "Copying files" -Id $childProgressId -Completed
                            }
                            Write-Progress -Activity "Copying files" -Id $progressId -Completed

                            # Return copied item if PassThru is specified
                            if ($PassThru) {
                                Get-Item -LiteralPath $targetPath
                            }
                        }
                    }
                }
            }
            catch {
                Write-Error -ErrorRecord $_
            }
        }
    }

    end {
        Write-Debug "End block started"
        # Restore console input handling
        [Console]::TreatControlCAsInput = $false
        Write-Debug "Function completed"
    }
}