src/Client/Update-XrmRecordFileUpload.ps1

<#
    .SYNOPSIS
    Upload a file to an entity record's file attribute field in Microsoft Dataverse.

    .Description
    Upload a file to a date row's (entity record's) file field from Microsoft Dataverse table.

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (CrmServiceClient)

    .PARAMETER Record
    Record (row) to update.

    .PARAMETER FileAttributeLogicalName
    Entity file attribute name.

    .PARAMETER FilePath
    Path to file on the OS file system.

    .EXAMPLE
    $xrmClient = New-XrmClient -ConnectionString $connectionString;
    # Create a new record that has a File attribute with the logical name: new_document
    $entityRecord = New-XrmEntity -LogicalName "new_DocumentStore" -Attributes @{
        "name" = "file1";
    }
    $entityRecord.Id = Add-XrmRecord -Record $entityRecord
    Update-XrmRecordFileUpload -XrmClient $XrmClient -Record $entityRecord -FileAttributeLogicalName "new_document" -FilePath 'C:\temp\test.docx'
#>

function Update-XrmRecordFileUpload
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [Microsoft.Xrm.Sdk.Entity]
        $Record,        

        [Parameter(Mandatory = $true)]
        [String]
        $FileAttributeLogicalName,

        [Parameter(Mandatory = $true)]
        [String]
        $FilePath
    )

    process 
    {
        # Created based on C# code exmaple from: https://gist.github.com/huseyinzengin91/8cbaa0ae3d11d985676560bbf470564f

        # File upload block size is limitted to 4MB
        $blockSizeByteLimit = 4194304
        $contentType = "application/octet-stream"

        $entityLogicalName = $Record.ToEntityReference().LogicalName
        [Guid]$recordId = $Record.ToEntityReference().Id

        # Get the filename from the filePath
        $fileName = [System.IO.Path]::GetFileName($FilePath)
        
        # # Lookup file's content type
        # # As of 2021-04-02 setting the content type to a value other than "application/octet-stream", doesn't change the content-type returned when downloading the file.
        # $fileExtension = [System.IO.Path]::GetExtension($FilePath)
        # Add-Type -AssemblyName "System.Web"
        # $contentType = [System.Web.MimeMapping]::GetMimeMapping($FilePath)

        # get the file size and calcualte the number of required parts
        $fileInfo = New-Object System.IO.FileInfo($FilePath)
        $totalFileBlocks = [int]($fileInfo.Length / $blockSizeByteLimit) + 1

        #Read the file and one chunk at a time
        $fileReader = [System.IO.File]::OpenRead($FilePath)
        $fileBlockCounter = 0
        $fileBuffer = New-Object Byte[] $blockSizeByteLimit
        $fileHasMoreData = $true

        # Keep an array of Block IDs to pass as part of the commit.
        $blockIds = [System.Collections.Generic.List[String]]::new()

        # Init file blocks upload request
        $initFileBlocksUploadRequest = [Microsoft.Crm.Sdk.Messages.InitializeFileBlocksUploadRequest]::new()
        $initFileBlocksUploadRequest.FileAttributeName = $FileAttributeLogicalName
        $initFileBlocksUploadRequest.FileName = $fileName
        $initFileBlocksUploadRequest.Target = $Record.ToEntityReference()

        $initResponse = $XrmClient.Execute($initFileBlocksUploadRequest)
        $fileContinuationToken = $initResponse.FileContinuationToken

        # Read file blocks until there is no more data
        while ($fileHasMoreData) 
        {
            # read a chuck of data form the file
            $fileBytesRead = $fileReader.Read($fileBuffer, 0, $fileBuffer.Length)
            $fileDataBlock = $fileBuffer

            # check if we read less than a full block of data from the file
            if($fileBytesRead -ne $fileBuffer.Length)
            {
                # If yes, then there is no more file data to read
                $fileHasMoreData = $false
                # truncate the output arrat to the number of bytes read
                $fileDataBlock = New-Object Byte[] $fileBytesRead
                [System.Array]::Copy($fileBuffer, $fileDataBlock, $fileBytesRead)
            }

            # Save the file block to the Dataverse entitie's file attribute

            # Generate a new random blockId and add it to the list of blockIds
            $blockId = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Guid]::NewGuid().ToString()))
            $blockIds.Add($blockId)

            $uploadBlockRequest = [Microsoft.Crm.Sdk.Messages.UploadBlockRequest]@{
                FileContinuationToken = $fileContinuationToken
                BlockId = $blockId
                BlockData = $fileDataBlock
            }

            # Upload a block
            $uploadBlockResponse = [Microsoft.Crm.Sdk.Messages.UploadBlockResponse]$XRMClient.Execute($uploadBlockRequest)

            $fileBlockCounter = $fileBlockCounter + 1
        }
        # Finished reading from the file. Close the reader
        $fileReader.Close()

        # Commit all file blocks
        $commitBlocksUploadRequest = [Microsoft.Crm.Sdk.Messages.CommitFileBlocksUploadRequest]@{
            BlockList = $blockIds.ToArray()
            FileContinuationToken = $fileContinuationToken
            FileName = $fileName
            MimeType = $contentType
        }
        $commitBlocksUploadResponse = [Microsoft.Crm.Sdk.Messages.CommitFileBlocksUploadResponse]$XrmClient.Execute($commitBlocksUploadRequest)

        #Write-Output "Xrm File upload complete. File name: $fileName, File size: $($fileInfo.Length), Blocks uploaded: $fileBlockCounter, FileId: $($commitBlocksUploadResponse.FileId)"
    }
}

Export-ModuleMember -Function Update-XrmRecordFileUpload -Alias *;