functions/public/Send-KlippyGcodeFile.ps1

function Send-KlippyGcodeFile {
    <#
    .SYNOPSIS
        Uploads a G-code file to a Klipper printer.

    .DESCRIPTION
        Uploads a local G-code file to the printer's gcodes storage.
        Supports uploading to subdirectories and optional print-after-upload.

    .PARAMETER Id
        The unique identifier of the printer.

    .PARAMETER PrinterName
        The friendly name of the printer.

    .PARAMETER InputObject
        A printer object from pipeline input.

    .PARAMETER FilePath
        Local path to the G-code file to upload.

    .PARAMETER Destination
        Destination path on the printer (relative to gcodes root).
        If not specified, uploads to root with original filename.

    .PARAMETER StartPrint
        Start printing the file immediately after upload.

    .PARAMETER PassThru
        Return the uploaded file object.

    .EXAMPLE
        Send-KlippyGcodeFile -FilePath "C:\prints\benchy.gcode"
        Uploads a file to the default printer.

    .EXAMPLE
        Send-KlippyGcodeFile -PrinterName "voronv2" -FilePath "./model.gcode" -Destination "projects/"
        Uploads to a specific folder.

    .EXAMPLE
        Send-KlippyGcodeFile -FilePath "urgent.gcode" -StartPrint
        Uploads and immediately starts printing.

    .EXAMPLE
        Get-ChildItem *.gcode | ForEach-Object { Send-KlippyGcodeFile -FilePath $_.FullName -PassThru }
        Uploads multiple files.

    .OUTPUTS
        PSCustomObject with file information (when -PassThru is used).
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ById')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByName')]
        [ValidateNotNullOrEmpty()]
        [string]$PrinterName,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByObject', ValueFromPipeline = $true)]
        [PSCustomObject]$InputObject,

        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

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

        [Parameter()]
        [switch]$StartPrint,

        [Parameter()]
        [switch]$PassThru
    )

    process {
        # Resolve printer
        $resolveParams = @{}
        switch ($PSCmdlet.ParameterSetName) {
            'ById' { $resolveParams['Id'] = $Id }
            'ByName' { $resolveParams['PrinterName'] = $PrinterName }
            'ByObject' { $resolveParams['InputObject'] = $InputObject }
        }

        $printer = Resolve-KlippyPrinterTarget @resolveParams

        # Resolve local file path
        $resolvedPath = Resolve-Path -Path $FilePath -ErrorAction Stop
        $fileInfo = Get-Item -Path $resolvedPath -ErrorAction Stop

        if ($fileInfo.PSIsContainer) {
            throw "Path '$FilePath' is a directory. Please specify a file."
        }

        # Determine destination filename
        $destFilename = $fileInfo.Name
        if ($Destination) {
            $cleanDest = $Destination.TrimStart('/').TrimEnd('/')
            if ($cleanDest -match '\.[^/]+$') {
                # Destination includes filename
                $destFilename = $cleanDest
            }
            else {
                # Destination is a folder
                $destFilename = "$cleanDest/$($fileInfo.Name)"
            }
        }

        $action = if ($StartPrint) { "Upload and start print" } else { "Upload file" }

        if ($PSCmdlet.ShouldProcess("$($fileInfo.FullName) -> $($printer.PrinterName):gcodes/$destFilename", $action)) {
            try {
                Write-Verbose "[$($printer.PrinterName)] Uploading: $($fileInfo.Name) -> $destFilename"

                # Build multipart form data
                $fileBytes = [System.IO.File]::ReadAllBytes($fileInfo.FullName)
                $boundary = [System.Guid]::NewGuid().ToString()

                $bodyLines = @(
                    "--$boundary",
                    "Content-Disposition: form-data; name=`"file`"; filename=`"$destFilename`"",
                    "Content-Type: application/octet-stream",
                    ""
                )

                # Build body with file content
                $headerBytes = [System.Text.Encoding]::UTF8.GetBytes(($bodyLines -join "`r`n") + "`r`n")
                $footerBytes = [System.Text.Encoding]::UTF8.GetBytes("`r`n--$boundary--`r`n")

                $bodyStream = [System.IO.MemoryStream]::new()
                $bodyStream.Write($headerBytes, 0, $headerBytes.Length)
                $bodyStream.Write($fileBytes, 0, $fileBytes.Length)
                $bodyStream.Write($footerBytes, 0, $footerBytes.Length)
                $bodyContent = $bodyStream.ToArray()
                $bodyStream.Dispose()

                # Build URI
                $uri = "$($printer.Uri)/server/files/upload"
                if ($StartPrint) {
                    $uri += "?print=true"
                }

                # Build headers
                $headers = @{
                    'Content-Type' = "multipart/form-data; boundary=$boundary"
                }
                if ($printer.ApiKey) {
                    $headers['X-Api-Key'] = $printer.ApiKey
                }

                # Upload file
                $response = Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -Body $bodyContent -TimeoutSec 300

                Write-Verbose "[$($printer.PrinterName)] Upload complete: $destFilename"

                if ($StartPrint) {
                    Write-Verbose "[$($printer.PrinterName)] Print started"
                }

                if ($PassThru) {
                    [PSCustomObject]@{
                        PSTypeName  = 'KlippyCLI.GcodeFile'
                        PrinterId   = $printer.Id
                        PrinterName = $printer.PrinterName
                        Path        = $response.item.path ?? $destFilename
                        Name        = Split-Path ($response.item.path ?? $destFilename) -Leaf
                        Size        = $fileInfo.Length
                        SizeMB      = [Math]::Round($fileInfo.Length / 1MB, 2)
                        Modified    = Get-Date
                    }
                }
            }
            catch {
                Write-Error "[$($printer.PrinterName)] Failed to upload '$($fileInfo.Name)': $_"
            }
        }
    }
}