Public/Storage/Send-VergeFile.ps1

function Send-VergeFile {
    <#
    .SYNOPSIS
        Uploads a file to the VergeOS media catalog.

    .DESCRIPTION
        Send-VergeFile uploads a local file (ISO, disk image, etc.) to the VergeOS
        files catalog where it can be used as a media source for VM drives.

        The upload uses chunked transfer for large files, displaying progress
        as the upload proceeds.

    .PARAMETER Path
        The local path to the file to upload.

    .PARAMETER Name
        The name to give the file in VergeOS. Defaults to the local filename.

    .PARAMETER Description
        Optional description for the uploaded file.

    .PARAMETER Tier
        The preferred storage tier (1-5) for the uploaded file.

    .PARAMETER Server
        The VergeOS connection to use. Defaults to the current default connection.

    .EXAMPLE
        Send-VergeFile -Path "C:\ISOs\ubuntu-22.04.iso"

        Uploads an ISO file to VergeOS using its original filename.

    .EXAMPLE
        Send-VergeFile -Path "/home/admin/server.iso" -Name "Ubuntu-Server-22.04.iso" -Tier 2

        Uploads a file with a custom name to tier 2 storage.

    .EXAMPLE
        Get-ChildItem *.iso | Send-VergeFile -Tier 1

        Uploads multiple ISO files to tier 1.

    .OUTPUTS
        Verge.File object representing the uploaded file.

    .NOTES
        Large files may take significant time to upload depending on network speed.
        The cmdlet shows progress during upload.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName', 'FilePath')]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$Path,

        [Parameter()]
        [ValidateLength(1, 255)]
        [string]$Name,

        [Parameter()]
        [ValidateLength(0, 2048)]
        [string]$Description,

        [Parameter()]
        [ValidateRange(1, 5)]
        [int]$Tier,

        [Parameter()]
        [object]$Server
    )

    begin {
        # Resolve connection
        if (-not $Server) {
            $Server = $script:DefaultConnection
        }
        if (-not $Server) {
            throw [System.InvalidOperationException]::new(
                'Not connected to VergeOS. Use Connect-VergeOS to establish a connection.'
            )
        }

        # Chunk size for uploads (256 KB - matches verge-cli)
        $script:ChunkSize = 262144
    }

    process {
        try {
            # Resolve full path
            $fullPath = Resolve-Path -Path $Path -ErrorAction Stop
            $fileInfo = Get-Item -Path $fullPath

            # Use provided name or default to filename
            $uploadName = if ($Name) { $Name } else { $fileInfo.Name }
            $fileSize = $fileInfo.Length

            $fileSizeMB = [math]::Round($fileSize / 1048576, 2)
            Write-Verbose "Uploading '$($fileInfo.Name)' ($fileSizeMB MB) to VergeOS as '$uploadName'"

            if ($PSCmdlet.ShouldProcess("File '$uploadName' ($fileSizeMB MB)", 'Upload to VergeOS')) {
                # Build authorization header
                $authType = if ($Server.AuthType) { $Server.AuthType } else { 'Basic' }
                $authHeader = "$authType $($Server.Token)"

                # Step 1: Create file entry with POST containing JSON metadata
                $uploadUrl = "$($Server.ApiBaseUrl)/files"

                $createBody = @{
                    allocated_bytes = $fileSize.ToString()
                    name            = $uploadName
                }

                if ($Description) {
                    $createBody['description'] = $Description
                }
                if ($Tier) {
                    $createBody['preferred_tier'] = $Tier
                }

                $createBodyJson = $createBody | ConvertTo-Json -Compress

                Write-Verbose "Creating file entry at: $uploadUrl"
                Write-Verbose "Request body: $createBodyJson"

                $createParams = @{
                    Method      = 'POST'
                    Uri         = $uploadUrl
                    Headers     = @{
                        'Authorization' = $authHeader
                        'Content-Type'  = 'application/json'
                    }
                    Body        = $createBodyJson
                    ErrorAction = 'Stop'
                }

                if ($Server.SkipCertificateCheck) {
                    $createParams['SkipCertificateCheck'] = $true
                }

                $createResponse = Invoke-RestMethod @createParams

                # Extract file ID from response
                $fileId = $null
                if ($createResponse.'$key') {
                    $fileId = $createResponse.'$key'
                }
                elseif ($createResponse.location) {
                    # Extract from location path
                    $fileId = ($createResponse.location -split '/')[-1]
                }

                if (-not $fileId) {
                    throw "Could not determine file ID from upload response"
                }

                Write-Verbose "File entry created with ID: $fileId"

                # Step 2: Upload file in chunks using PUT
                $totalChunks = [math]::Ceiling($fileSize / $script:ChunkSize)
                $uploadedChunks = 0

                $fileStream = [System.IO.File]::OpenRead($fullPath)
                try {
                    $offset = [int64]0

                    while ($offset -lt $fileSize) {
                        $bytesToRead = [math]::Min($script:ChunkSize, $fileSize - $offset)

                        # Create a fresh buffer for each chunk
                        $buffer = New-Object byte[] $bytesToRead
                        $bytesRead = $fileStream.Read($buffer, 0, $bytesToRead)

                        if ($bytesRead -eq 0) {
                            break
                        }

                        # Build chunk URL with filepos parameter
                        $chunkUrl = "$uploadUrl/$fileId`?filepos=$offset"

                        # Use WebRequest for more reliable streaming
                        $chunkParams = @{
                            Method      = 'PUT'
                            Uri         = $chunkUrl
                            Headers     = @{
                                'Authorization' = $authHeader
                                'Content-Type'  = 'application/octet-stream'
                            }
                            Body        = $buffer
                            ErrorAction = 'Stop'
                        }

                        if ($Server.SkipCertificateCheck) {
                            $chunkParams['SkipCertificateCheck'] = $true
                        }

                        $null = Invoke-WebRequest @chunkParams

                        $offset += $bytesRead
                        $uploadedChunks++

                        # Update progress
                        $percentComplete = [math]::Round(($uploadedChunks / $totalChunks) * 100)
                        $uploadedMB = [math]::Round($offset / 1048576, 2)
                        Write-Progress -Activity "Uploading $uploadName" `
                            -Status "$uploadedMB MB of $fileSizeMB MB ($percentComplete%)" `
                            -PercentComplete $percentComplete
                    }
                }
                finally {
                    $fileStream.Close()
                    $fileStream.Dispose()
                    Write-Progress -Activity "Uploading $uploadName" -Completed
                }

                Write-Verbose "Upload completed successfully"

                # Return the file info
                Get-VergeFile -Key $fileId -Server $Server
            }
        }
        catch {
            $fileName = if ($uploadName) { $uploadName } else { $Path }
            Write-Error -Message "Failed to upload file '$fileName': $($_.Exception.Message)" -ErrorId 'SendVergeFileFailed'
        }
    }
}