functions/Invoke-TFTPRequest.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Native PowerShell TFTP client — upload and download files without tftp.exe.

.DESCRIPTION
    Implements RFC 1350 (TFTP Protocol Revision 2) plus RFC 2347/2348 option
    extensions (blksize, windowsize). All I/O uses System.Net.Sockets.UdpClient;
    no external executables are required. Progress is printed to the console in
    a curl-like style.

.PARAMETER Server
    Hostname or IP address of the TFTP server.

.PARAMETER RemoteFile
    Path / filename as it should appear on the server side.

.PARAMETER LocalFile
    Local filesystem path to read from (Upload) or write to (Download).

.PARAMETER Operation
    'Download' (default) or 'Upload'.

.PARAMETER Port
    UDP port on the server. Default: 69.

.PARAMETER Mode
    Transfer mode: 'octet' (binary, default) or 'netascii'.

.PARAMETER BlockSize
    Payload bytes per DATA packet (RFC 2348). Range 8-65464. Default: 512.

.PARAMETER TimeoutSeconds
    Per-packet ACK/DATA wait timeout in seconds. Default: 5.

.PARAMETER Retries
    How many times to re-send a packet before giving up. Default: 5.

.PARAMETER WindowSize
    Unacknowledged blocks in flight (RFC 7440). Default: 1 (stop-and-wait).

.PARAMETER PassThru
    Download: return [byte[]] instead of writing a file.
    Upload: accept [byte[]] from pipeline instead of reading a file.

.EXAMPLE
    Invoke-TFTPRequest -Server 192.168.1.1 -RemoteFile 'firmware.bin' -LocalFile 'C:\firmware.bin'

.EXAMPLE
    Invoke-TFTPRequest -Server 192.168.1.1 -RemoteFile 'backup.cfg' -LocalFile 'C:\backup.cfg' -Operation Upload -BlockSize 1468
#>

function Invoke-TFTPRequest {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([byte[]])]
    param(
        [Parameter(Mandatory)][string]$Server,
        [Parameter(Mandatory)][string]$RemoteFile,
        [Parameter()][string]$LocalFile,
        [Parameter()][ValidateSet('Download','Upload')][string]$Operation = 'Download',
        [Parameter()][ValidateRange(1,65535)][int]$Port = 69,
        [Parameter()][ValidateSet('octet','netascii')][string]$Mode = 'octet',
        [Parameter()][ValidateRange(8,65464)][int]$BlockSize = 512,
        [Parameter()][ValidateRange(1,60)][int]$TimeoutSeconds = 5,
        [Parameter()][ValidateRange(1,10)][int]$Retries = 5,
        [Parameter()][ValidateRange(1,65535)][int]$WindowSize = 1,
        [Parameter(ValueFromPipeline)][switch]$PassThru
    )

    # ── Opcodes (RFC 1350 §5) ────────────────────────────────────────────────
    $OP_RRQ   = [uint16]1
    $OP_WRQ   = [uint16]2
    $OP_DATA  = [uint16]3
    $OP_ACK   = [uint16]4
    $OP_ERROR = [uint16]5
    $OP_OACK  = [uint16]6

    $ErrorMessages = @{
        0='Not defined'; 1='File not found'; 2='Access violation'
        3='Disk full'; 4='Illegal TFTP operation'; 5='Unknown transfer ID'
        6='File already exists'; 7='No such user'; 8='Failed to negotiate options'
    }

    # ── Helpers ──────────────────────────────────────────────────────────────

    function NullTerm([string]$s) {
        $b = [System.Text.Encoding]::ASCII.GetBytes($s)
        $r = [byte[]]::new($b.Length + 1)
        [Buffer]::BlockCopy($b, 0, $r, 0, $b.Length)
        $r[$b.Length] = 0
        return $r
    }

    function NextBlock([uint16]$n) {
        if ($n -ge 65535) { return [uint16]0 } else { return [uint16]($n + 1) }
    }

    function New-ReqPacket([uint16]$op, [string]$file, [string]$mode, [int]$blk, [int]$win) {
        $ms = [System.IO.MemoryStream]::new()
        $ms.WriteByte([byte]($op -shr 8)); $ms.WriteByte([byte]($op -band 0xFF))
        foreach ($b in (NullTerm $file)) { $ms.WriteByte($b) }
        foreach ($b in (NullTerm $mode)) { $ms.WriteByte($b) }
        if ($blk -ne 512) {
            foreach ($b in (NullTerm 'blksize'))  { $ms.WriteByte($b) }
            foreach ($b in (NullTerm "$blk"))     { $ms.WriteByte($b) }
        }
        if ($win -gt 1) {
            foreach ($b in (NullTerm 'windowsize')) { $ms.WriteByte($b) }
            foreach ($b in (NullTerm "$win"))       { $ms.WriteByte($b) }
        }
        return $ms.ToArray()
    }

    function New-AckPacket([uint16]$n) {
        [byte[]]@([byte]($OP_ACK -shr 8), [byte]($OP_ACK -band 0xFF),
                  [byte]($n -shr 8),      [byte]($n -band 0xFF))
    }

    function New-DataPacket([uint16]$n, [byte[]]$data) {
        $hdr = [byte[]]@([byte]($OP_DATA -shr 8), [byte]($OP_DATA -band 0xFF),
                         [byte]($n -shr 8),        [byte]($n -band 0xFF))
        $pkt = [byte[]]::new(4 + $data.Length)
        [Buffer]::BlockCopy($hdr,  0, $pkt, 0, 4)
        [Buffer]::BlockCopy($data, 0, $pkt, 4, $data.Length)
        return $pkt
    }

    function New-ErrPacket([uint16]$code, [string]$msg) {
        $tail = NullTerm $msg
        $pkt  = [byte[]]::new(4 + $tail.Length)
        $pkt[0] = [byte]($OP_ERROR -shr 8); $pkt[1] = [byte]($OP_ERROR -band 0xFF)
        $pkt[2] = [byte]($code -shr 8);     $pkt[3] = [byte]($code -band 0xFF)
        [Buffer]::BlockCopy($tail, 0, $pkt, 4, $tail.Length)
        return $pkt
    }

    function ReadU16([byte[]]$b, [int]$i) {
        (([int]$b[$i]) -shl 8) -bor [int]$b[$i+1]
    }

    function Get-ErrMsg([byte[]]$pkt) {
        $ec   = ReadU16 $pkt 2
        $mlen = [math]::Max(0, $pkt.Length - 5)
        $em   = if ($mlen -gt 0) { [System.Text.Encoding]::ASCII.GetString($pkt, 4, $mlen) } else { '' }
        return "Server error $ec ($($ErrorMessages[$ec])): $em"
    }

    # Returns @{Data=[byte[]]; EP=[IPEndPoint]} or $null on timeout
    function Recv([System.Net.Sockets.UdpClient]$u, [int]$ms) {
        $u.Client.ReceiveTimeout = $ms
        $ep = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
        try {
            $data = $u.Receive([ref]$ep)
            return @{ Data = $data; EP = $ep }
        } catch [System.Net.Sockets.SocketException] {
            if ($_.Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError]::TimedOut) {
                return $null
            }
            throw
        }
    }

    function Parse-OAck([byte[]]$pkt) {
        $opts = @{}; $i = 2
        while ($i -lt $pkt.Length) {
            $end = [Array]::IndexOf($pkt, [byte]0, $i)
            if ($end -lt 0) { break }
            $key = [System.Text.Encoding]::ASCII.GetString($pkt, $i, $end - $i).ToLower()
            $i = $end + 1
            $end = [Array]::IndexOf($pkt, [byte]0, $i)
            if ($end -lt 0) { $end = $pkt.Length }
            $opts[$key] = [System.Text.Encoding]::ASCII.GetString($pkt, $i, $end - $i)
            $i = $end + 1
        }
        return $opts
    }

    # ── Progress helpers ─────────────────────────────────────────────────────

    function Format-Size([long]$bytes) {
        if ($bytes -ge 1GB) { return '{0:F2} GiB' -f ($bytes / 1GB) }
        if ($bytes -ge 1MB) { return '{0:F2} MiB' -f ($bytes / 1MB) }
        if ($bytes -ge 1KB) { return '{0:F2} KiB' -f ($bytes / 1KB) }
        return "$bytes B"
    }

    function Format-Speed([double]$Bps) {
        if ($Bps -ge 1GB) { return '{0:F2} GiB/s' -f ($Bps / 1GB) }
        if ($Bps -ge 1MB) { return '{0:F2} MiB/s' -f ($Bps / 1MB) }
        if ($Bps -ge 1KB) { return '{0:F2} KiB/s' -f ($Bps / 1KB) }
        return ('{0:F0} B/s' -f $Bps)
    }

    function Format-Time([double]$sec) {
        if ($sec -lt 0 -or $sec -gt 359999) { return '--:--:--' }
        $h = [int]($sec / 3600); $m = [int](($sec % 3600) / 60); $s = [int]($sec % 60)
        return '{0:D2}:{1:D2}:{2:D2}' -f $h, $m, $s
    }

    function Show-Progress([string]$op, [long]$done, [long]$total, [double]$Bps) {
        $doneStr  = Format-Size $done
        $speedStr = Format-Speed $Bps

        if ($total -gt 0) {
            $pct = [int]([math]::Min(100, $done * 100 / $total))
            $eta = if ($Bps -gt 0) { [int](($total - $done) / $Bps) } else { -1 }
            Write-Progress -Activity "TFTP $op : $RemoteFile" `
                           -Status ('{0} / {1} ({2})' -f $doneStr, (Format-Size $total), $speedStr) `
                           -PercentComplete $pct `
                           -SecondsRemaining $eta
        } else {
            Write-Progress -Activity "TFTP $op : $RemoteFile" `
                           -Status "$doneStr transferred ($speedStr)" `
                           -PercentComplete -1
        }
    }

    function Show-Summary([string]$op, [long]$bytes, [double]$elapsed) {
        Write-Progress -Activity "TFTP $op : $RemoteFile" -Completed
        $speed = if ($elapsed -gt 0) { $bytes / $elapsed } else { 0 }
        Write-Host (' {0} complete. {1} in {2} ({3})' -f `
            $op, (Format-Size $bytes), (Format-Time $elapsed), (Format-Speed $speed)) `
            -ForegroundColor Green
    }

    function Show-Header([string]$op, [string]$server, [string]$remote, [string]$local) {
        Write-Host ""
        Write-Host (" {0,-10} {1}" -f "${op}:", $remote) -ForegroundColor Cyan
        Write-Host (" {0,-10} {1}:{2}" -f "Server:", $server, $Port)
        if ($local) { Write-Host (" {0,-10} {1}" -f "Local:", $local) }
        Write-Host ""
    }

    # ── Argument validation ──────────────────────────────────────────────────

    if ($Operation -eq 'Download' -and -not $PassThru -and -not $LocalFile) {
        throw 'Specify -LocalFile or -PassThru for Download.'
    }
    if ($Operation -eq 'Upload' -and -not $PassThru -and -not $LocalFile) {
        throw 'Specify -LocalFile or -PassThru for Upload.'
    }
    if ($Operation -eq 'Upload' -and $LocalFile -and -not (Test-Path $LocalFile)) {
        throw "Upload source not found: $LocalFile"
    }

    # ── Resolve server ───────────────────────────────────────────────────────

    try {
        $srvAddr = [System.Net.Dns]::GetHostAddresses($Server) |
                   Where-Object { $_.AddressFamily -eq 'InterNetwork' } |
                   Select-Object -First 1
        if (-not $srvAddr) { throw 'No IPv4 address.' }
    } catch { throw "Cannot resolve '$Server': $_" }

    $serverEP = [System.Net.IPEndPoint]::new($srvAddr, $Port)

    # ── Load upload data ─────────────────────────────────────────────────────

    [byte[]]$uploadData = $null
    if ($Operation -eq 'Upload') {
        if ($PassThru -and $input) { $uploadData = $input }
        else { $uploadData = [System.IO.File]::ReadAllBytes((Resolve-Path $LocalFile).Path) }
    }

    # ── Open socket ──────────────────────────────────────────────────────────

    $udp    = [System.Net.Sockets.UdpClient]::new(
                  [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0))
    $tmsOut = $TimeoutSeconds * 1000
    $reqPkt = New-ReqPacket `
                  ($(if ($Operation -eq 'Download') { $OP_RRQ } else { $OP_WRQ })) `
                  $RemoteFile $Mode $BlockSize $WindowSize

    Show-Header $Operation $Server $RemoteFile $LocalFile

    try {
        if (-not $PSCmdlet.ShouldProcess("$Server`:$Port", "$Operation $RemoteFile")) { return }

        # Send the initial RRQ / WRQ
        $null = $udp.Send($reqPkt, $reqPkt.Length, $serverEP)
        Write-Verbose "Sent $(if ($Operation -eq 'Download') { 'RRQ' } else { 'WRQ' }) to $($serverEP)"

        # ── DOWNLOAD ─────────────────────────────────────────────────────────
        if ($Operation -eq 'Download') {

            $recvBlocks    = [System.Collections.Generic.List[byte[]]]::new()
            $expectedBlock = [uint16]1
            $effectiveBlk  = $BlockSize
            $done          = $false
            $totalReceived = [long]0
            $knownTotal    = [long]0
            $sw            = [System.Diagnostics.Stopwatch]::StartNew()

            # ── First reply: OACK or DATA 1 ──────────────────────────────────
            $attempt  = 0
            $firstRes = $null
            while (-not $firstRes -and $attempt -lt $Retries) {
                $firstRes = Recv $udp $tmsOut
                if (-not $firstRes) {
                    $attempt++
                    Write-Verbose "No first reply, retry $attempt/$Retries"
                    $null = $udp.Send($reqPkt, $reqPkt.Length, $serverEP)
                }
            }
            if (-not $firstRes) { throw "No response from $Server after $Retries retries." }

            $firstPkt  = $firstRes.Data
            $remEP     = $firstRes.EP
            $serverTID = $remEP.Port
            $firstOp   = ReadU16 $firstPkt 0

            Write-Verbose "First reply: opcode=$firstOp from $($remEP.Address):$($remEP.Port)"

            if ($firstOp -eq $OP_ERROR) { throw (Get-ErrMsg $firstPkt) }

            if ($firstOp -eq $OP_OACK) {
                $opts = Parse-OAck $firstPkt
                if ($opts['blksize'])    { $effectiveBlk = [int]$opts['blksize'] }
                if ($opts['windowsize']) { $WindowSize   = [int]$opts['windowsize'] }
                if ($opts['tsize'])      { $knownTotal   = [long]$opts['tsize'] }
                Write-Verbose "OACK: blksize=$effectiveBlk win=$WindowSize tsize=$knownTotal"
                $ack = New-AckPacket 0
                $null = $udp.Send($ack, $ack.Length, $remEP)
            }
            elseif ($firstOp -eq $OP_DATA) {
                $bn      = ReadU16 $firstPkt 2
                if ($bn -ne 1) { throw "Expected DATA 1, got block $bn" }
                $payload = [byte[]]::new($firstPkt.Length - 4)
                [Array]::Copy($firstPkt, 4, $payload, 0, $payload.Length)
                $recvBlocks.Add($payload)
                $totalReceived += $payload.Length
                if ($payload.Length -lt $effectiveBlk) { $done = $true }
                $ack = New-AckPacket 1
                $null = $udp.Send($ack, $ack.Length, $remEP)
                $expectedBlock = NextBlock 1
                Write-Verbose "DATA 1 received ($($payload.Length) bytes)"

                $elapsed = $sw.Elapsed.TotalSeconds
                $Bps     = if ($elapsed -gt 0) { $totalReceived / $elapsed } else { 0 }
                Show-Progress 'Download' $totalReceived $knownTotal $Bps
            }
            else { throw "Unexpected opcode $firstOp" }

            # ── Main download loop ────────────────────────────────────────────
            while (-not $done) {
                $windowReceived = 0
                $lastAcked      = if ($expectedBlock -eq 0) { [uint16]65535 } else { [uint16]($expectedBlock - 1) }

                while ($windowReceived -lt $WindowSize -and -not $done) {
                    $attempt = 0
                    $res     = $null
                    while (-not $res -and $attempt -lt $Retries) {
                        $res = Recv $udp $tmsOut
                        if (-not $res) {
                            $attempt++
                            Write-Verbose "Timeout waiting for block $expectedBlock, re-ACK $lastAcked (retry $attempt/$Retries)"
                            $ack = New-AckPacket $lastAcked
                            $null = $udp.Send($ack, $ack.Length, $remEP)
                        }
                    }
                    if (-not $res) { throw "Stalled at block $expectedBlock after $Retries retries." }

                    $pkt = $res.Data
                    $ep  = $res.EP

                    if ($ep.Port -ne $serverTID) {
                        $errPkt = New-ErrPacket 5 'Unknown TID'
                        $null = $udp.Send($errPkt, $errPkt.Length, $ep)
                        continue
                    }

                    $op = ReadU16 $pkt 0
                    if ($op -eq $OP_ERROR) { throw (Get-ErrMsg $pkt) }
                    if ($op -ne $OP_DATA)  { throw "Expected DATA, got opcode $op" }

                    $bn = ReadU16 $pkt 2
                    if ($bn -eq $expectedBlock) {
                        $payload = [byte[]]::new($pkt.Length - 4)
                        [Array]::Copy($pkt, 4, $payload, 0, $payload.Length)
                        $recvBlocks.Add($payload)
                        $totalReceived += $payload.Length

                        if ($payload.Length -lt $effectiveBlk) { $done = $true }
                        $lastAcked     = $bn
                        $expectedBlock = NextBlock $bn
                        $windowReceived++

                        Write-Verbose "DATA $bn received ($($payload.Length) bytes, total $totalReceived)"

                        $elapsed = $sw.Elapsed.TotalSeconds
                        $Bps     = if ($elapsed -gt 0) { $totalReceived / $elapsed } else { 0 }
                        Show-Progress 'Download' $totalReceived $knownTotal $Bps
                    }
                    else {
                        Write-Verbose "Out-of-order block $bn (expected $expectedBlock), ignoring"
                    }
                }

                $ack = New-AckPacket $lastAcked
                $null = $udp.Send($ack, $ack.Length, $remEP)
            }

            $elapsed = $sw.Elapsed.TotalSeconds

            $outBuf = [byte[]]::new($totalReceived)
            $pos    = 0
            foreach ($blk in $recvBlocks) {
                [Buffer]::BlockCopy($blk, 0, $outBuf, $pos, $blk.Length)
                $pos += $blk.Length
            }

            Show-Summary 'Download' $totalReceived $elapsed

            if ($PassThru) { return $outBuf }
            else {
                [System.IO.File]::WriteAllBytes($LocalFile, $outBuf)
                Write-Host " Saved to $LocalFile" -ForegroundColor Gray
            }
        }

        # ── UPLOAD ───────────────────────────────────────────────────────────
        else {
            $effectiveBlk = $BlockSize
            $totalBytes   = [long]$uploadData.Length
            $sw           = [System.Diagnostics.Stopwatch]::StartNew()

            # ── Wait for ACK 0 / OACK ─────────────────────────────────────────
            $attempt  = 0
            $firstRes = $null
            while (-not $firstRes -and $attempt -lt $Retries) {
                $firstRes = Recv $udp $tmsOut
                if (-not $firstRes) {
                    $attempt++
                    Write-Verbose "No WRQ reply, retry $attempt/$Retries"
                    $null = $udp.Send($reqPkt, $reqPkt.Length, $serverEP)
                }
            }
            if (-not $firstRes) { throw "No response from $Server after $Retries retries." }

            $firstPkt = $firstRes.Data
            $sendEP   = $firstRes.EP
            Write-Verbose "Server TID $($sendEP.Address):$($sendEP.Port)"

            $firstOp = ReadU16 $firstPkt 0

            if ($firstOp -eq $OP_ERROR) { throw (Get-ErrMsg $firstPkt) }
            elseif ($firstOp -eq $OP_OACK) {
                $opts = Parse-OAck $firstPkt
                if ($opts['blksize'])    { $effectiveBlk = [int]$opts['blksize'] }
                if ($opts['windowsize']) { $WindowSize   = [int]$opts['windowsize'] }
                Write-Verbose "OACK: blksize=$effectiveBlk win=$WindowSize"
            }
            elseif ($firstOp -eq $OP_ACK) {
                $ab = ReadU16 $firstPkt 2
                if ($ab -ne 0) { throw "Expected ACK 0, got ACK $ab" }
                Write-Verbose "ACK 0 received — starting transfer (blksize=$effectiveBlk)"
            }
            else { throw "Expected ACK 0 or OACK, got opcode $firstOp" }

            # Show initial progress bar at 0%
            Show-Progress 'Upload' 0 $totalBytes 0

            # ── Main upload loop ──────────────────────────────────────────────
            # blockNum: the block number of the FIRST packet in the current window.
            # Inside the window loop we only advance blockNum between packets, never
            # for the last packet of a window — that increment happens in the outer
            # else-branch so the next window starts on the correct number.
            $blockNum = [uint16]1
            $offset   = [long]0
            $done     = $false

            while (-not $done) {
                $windowPkts   = [System.Collections.Generic.List[byte[]]]::new()
                $isLastWindow = $false

                # Build window: collect up to $WindowSize DATA packets.
                # blockNum is incremented between packets (not after the last one),
                # so after the loop $blockNum == the last block number sent this window.
                for ($w = 0; $w -lt $WindowSize; $w++) {
                    $remaining = $totalBytes - $offset
                    $chunkSize = [int][math]::Min($effectiveBlk, [math]::Max(0, $remaining))

                    $chunk = [byte[]]::new($chunkSize)
                    if ($chunkSize -gt 0) {
                        [Buffer]::BlockCopy($uploadData, $offset, $chunk, 0, $chunkSize)
                        $offset += $chunkSize
                    }

                    $windowPkts.Add((New-DataPacket $blockNum $chunk))
                    Write-Verbose " Queuing DATA block $blockNum ($chunkSize bytes, offset $offset / $totalBytes)"

                    if ($chunkSize -lt $effectiveBlk) {
                        $isLastWindow = $true
                        break
                    }

                    if ($w -lt $WindowSize - 1) {
                        $blockNum = NextBlock $blockNum
                    }
                }

                # $blockNum now equals the block number of the last packet sent
                $expectedAck = $blockNum

                # Send the window
                foreach ($dp in $windowPkts) {
                    $null = $udp.Send($dp, $dp.Length, $sendEP)
                }
                Write-Verbose "Sent window, last block $blockNum, awaiting ACK $expectedAck"

                # Wait for expected ACK. Stale ACKs (leftover from previous windows) are
                # discarded without counting as a retry — only genuine timeouts increment $attempt.
                $attempt      = 0
                $ackConfirmed = $false
                while (-not $ackConfirmed -and $attempt -lt $Retries) {
                    $r = Recv $udp $tmsOut
                    if (-not $r) {
                        $attempt++
                        Write-Verbose "Timeout waiting for ACK $expectedAck (retry $attempt/$Retries) — retransmitting window"
                        foreach ($dp in $windowPkts) {
                            $null = $udp.Send($dp, $dp.Length, $sendEP)
                        }
                        continue
                    }

                    if ($r.EP.Port -ne $sendEP.Port) {
                        $errPkt = New-ErrPacket 5 'Unknown TID'
                        $null = $udp.Send($errPkt, $errPkt.Length, $r.EP)
                        Write-Verbose "Discarding packet from unknown TID $($r.EP.Port) (expected $($sendEP.Port))"
                        continue
                    }

                    $ackPkt = $r.Data
                    $ackOp  = ReadU16 $ackPkt 0
                    if ($ackOp -eq $OP_ERROR) { throw (Get-ErrMsg $ackPkt) }
                    if ($ackOp -ne $OP_ACK) {
                        Write-Verbose "Discarding non-ACK opcode $ackOp"
                        continue
                    }

                    $ackedBlock = ReadU16 $ackPkt 2
                    Write-Verbose "Received ACK $ackedBlock (expected $expectedAck)"

                    if ($ackedBlock -eq $expectedAck) {
                        $ackConfirmed = $true
                    } else {
                        Write-Verbose "Stale ACK $ackedBlock discarded (expected $expectedAck)"
                    }
                }
                if (-not $ackConfirmed) { throw "Upload stalled after $Retries retries at block $expectedAck." }

                # Update progress on every window ACK
                $elapsed = $sw.Elapsed.TotalSeconds
                $Bps     = if ($elapsed -gt 0) { $offset / $elapsed } else { 0 }
                Show-Progress 'Upload' $offset $totalBytes $Bps

                if ($isLastWindow) {
                    $done = $true
                } else {
                    $blockNum = NextBlock $blockNum
                }
            }

            Show-Summary 'Upload' $totalBytes $sw.Elapsed.TotalSeconds
        }
    }
    finally {
        $udp.Close()
        $udp.Dispose()
    }
}