Private/Send-NSPScanBridgeUploadSession.ps1

function Send-NSPScanBridgeUploadSession {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)][string]$DriveId,
        [Parameter(Mandatory)][string]$FolderItemId,
        [Parameter(Mandatory)][string]$Name
    )
    $info = Get-Item -LiteralPath $Path -ErrorAction Stop
    if ($info.Length -eq 0) { throw 'Cannot upload an empty file.' }
    $drive = [uri]::EscapeDataString($DriveId)
    $parent = [uri]::EscapeDataString($FolderItemId)
    $encodedName = [uri]::EscapeDataString($Name)
    $uri = "drives/$drive/items/$($parent):/$($encodedName):/createUploadSession"
    $session = $null
    for ($attempt = 1; $attempt -le 3; $attempt++) {
        try {
            $session = Invoke-NSPGraphRequest -Method POST -Uri $uri -Body @{
                # SharePoint requires the conflict annotation before ordinary properties.
                # An unordered hashtable can serialize name first and produce HTTP 400.
                item = [ordered]@{ '@microsoft.graph.conflictBehavior' = 'fail'; name = $Name }
            } -ErrorAction Stop
            break
        } catch {
            # Only retry documented transient HTTP responses before bytes are sent.
            $status = 0
            if ($null -ne $_.Exception.ResponseStatusCode) {
                $status = [int]$_.Exception.ResponseStatusCode
            } elseif ($null -ne $_.Exception.Response -and $null -ne $_.Exception.Response.StatusCode) {
                $status = [int]$_.Exception.Response.StatusCode
            }
            if ($attempt -eq 3 -or -not (Test-NSPScanBridgeRetrySafe -Phase BeforeSession -StatusCode $status)) {
                throw
            }
            Start-Sleep -Seconds (Get-NSPScanBridgeBackoff -Attempt $attempt)
        }
    }
    if (-not $session.uploadUrl -or $session.uploadUrl -notmatch '^https://') {
        throw 'Graph did not provide an HTTPS upload URL.'
    }
    $chunkSize = 327680 * 16
    $stream = [IO.File]::Open($info.FullName, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
    try {
        $position = [long]0
        while ($position -lt $stream.Length) {
            $count = [int][math]::Min($chunkSize, $stream.Length - $position)
            $bytes = New-Object byte[] $count
            $read = 0
            while ($read -lt $count) {
                $n = $stream.Read($bytes, $read, $count - $read)
                if ($n -eq 0) { throw 'Unexpected end of file during upload.' }
                $read += $n
            }
            $end = $position + $count - 1
            $headers = @{ 'Content-Range' = "bytes $position-$end/$($stream.Length)" }
            $args = @{ Method = 'PUT'; Uri = $session.uploadUrl; Body = $bytes; ContentType = 'application/octet-stream'; Headers = $headers; ErrorAction = 'Stop' }
            $response = Invoke-RestMethod @args
            $position += $count
            if ($position -lt $stream.Length) {
                $ranges = @($response.nextExpectedRanges)
                # This worker does not resume or replay chunks. Hold any unexpected range.
                if ($ranges.Count -ne 1 -or [string]$ranges[0] -notmatch ('^' + $position + '-(?:[0-9]+)?$')) {
                    throw 'Graph did not acknowledge the sequential upload position.'
                }
                if ([string]$ranges[0] -match '-([0-9]+)$' -and
                    ([decimal]$Matches[1] -lt $position -or [decimal]$Matches[1] -ge $stream.Length)) {
                    throw 'Graph returned an invalid remaining byte range.'
                }
            }
        }
        if (-not $response.id -or [long]$response.size -ne $stream.Length) {
            throw 'Final upload response did not confirm drive item and byte size.'
        }
        return $response
    } finally { $stream.Dispose() }
}