Public/Send-FitFileToGarminConnect.ps1

<#
.SYNOPSIS
Uploads FIT files from a local folder to Garmin Connect.
 
.DESCRIPTION
`Send-FitFileToGarminConnect` validates the selected import directory, checks for
available `.fit` files, and uploads each file to the Garmin Connect API endpoint
`/upload-service/upload` directly from PowerShell.
 
Authentication uses the DI OAuth2 token store written by `Get-GarminToken`
(`garmin_tokens.json`). An access token that expires within 15 minutes is
refreshed automatically and saved back.
 
The files are uploaded in alphabetical order. A failed upload does not stop the
remaining uploads; every file is reported with its own status:
- Uploaded: Garmin Connect accepted the file.
- Duplicate: Garmin Connect already contains this data (HTTP 409).
- Failed: The upload failed; `Message` contains the reason.
 
Returns one result object per file with these properties:
- File: Name of the FIT file.
- Status: `Uploaded`, `Duplicate` or `Failed`.
- Message: Error message or Garmin import message, empty when uploaded.
 
.PARAMETER ImportDirectory
Path to the folder containing `.fit` files.
If omitted, the module default FIT folder is used (`C:\Users\Public\GCCare\FitFiles`).
 
.PARAMETER TokenStore
Optional path to the Garmin Connect token store directory or `garmin_tokens.json` file.
Defaults to `$env:GARMINTOKENS`, then `~\.garminconnect`.
 
.PARAMETER EnableLogging
Optional switch to enable module logging behavior (if supported by module logging helpers).
 
.OUTPUTS
System.Management.Automation.PSCustomObject
Returns one result object (`File`, `Status`, `Message`) per uploaded FIT file.
Throws when the import directory does not exist or contains no `.fit` files.
 
.NOTES
- Requires helper functions in module scope:
  `Get-FunctionName`, `Write-Log`, `Invoke-Output`, `Get-RunTime`, `Invoke-GarminConnectApi`.
- Requires a token file created by `Get-GarminToken`.
- FIT files can be created with `Convert-TanitaExportToFitFile`.
 
.EXAMPLE
Send-FitFileToGarminConnect
 
Uploads all FIT files from the default folder C:\Users\Public\GCCare\FitFiles.
 
.EXAMPLE
Send-FitFileToGarminConnect -ImportDirectory "C:\Data\Garmin\Fit"
 
Uploads all FIT files from the specified folder.
 
.EXAMPLE
Send-FitFileToGarminConnect -ImportDirectory "C:\Data\Garmin\Fit" -TokenStore "C:\Temp\.garminconnect"
 
Uploads all FIT files from the specified folder using a custom token store.
 
.EXAMPLE
Send-FitFileToGarminConnect | Where-Object Status -eq 'Failed'
 
Uploads all FIT files and shows only the files that could not be uploaded.
#>

Function Send-FitFileToGarminConnect {

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$ImportDirectory = $Script:DefaultFitFilesFolder,
        [string]$TokenStore,
        [switch]$EnableLogging
    )

    $CurrentFunction = Get-FunctionName
    Write-Log -Message "### Start Function $CurrentFunction ###"
    $StartRunTime = (Get-Date).ToString($Script:DateFormatLog)
    #################### main code | out- host #####################

    Invoke-Output -Type Header -Message "Uploading FIT files to Garmin Connect..."

    if (-not $ImportDirectory) {
        $ImportDirectory = $Script:DefaultFitFilesFolder
    }

    $ImportDirectory = [System.IO.Path]::GetFullPath($ImportDirectory)

    if (-not (Test-Path -LiteralPath $ImportDirectory -PathType Container)) {
        throw "Import directory not found: $ImportDirectory"
    }

    $fitFiles = @(Get-ChildItem -LiteralPath $ImportDirectory -Filter '*.fit' -File -ErrorAction SilentlyContinue | Sort-Object -Property Name)
    if ($fitFiles.Count -eq 0) {
        throw "No .fit files found in import directory: $ImportDirectory"
    }

    Invoke-Output -Type Bullet -Message "Import directory: " -TextMaker $ImportDirectory -NoExtraLines
    Invoke-Output -Type Bullet -Message "FIT files found: " -TextMaker $fitFiles.Count

    $results = foreach ($fitFile in $fitFiles) {
        $status = 'Uploaded'
        $message = ''

        try {
            $response = Invoke-GarminConnectApi -Path '/upload-service/upload' -Method Post -Form @{ file = $fitFile } -TokenStore $TokenStore

            # Garmin reports rejected files inside the import result
            $failures = @($response.detailedImportResult.failures | Where-Object { $_ })
            if ($failures.Count -gt 0) {
                $status = 'Failed'
                $message = (@($failures.messages.content | Where-Object { $_ }) -join '; ')
                if ($message -match '(?i)duplicate') {
                    $status = 'Duplicate'
                }
            }
        }
        catch {
            $message = $_.Exception.Message
            $status = if ($message -match '\(409\)') { 'Duplicate' } else { 'Failed' }
        }

        switch ($status) {
            'Uploaded' { Invoke-Output -Type Bullet -Message "Uploaded: " -TextMaker $fitFile.Name -NoExtraLines }
            'Duplicate' { Invoke-Output -Type Bullet -Message "Duplicate: " -TextMaker $fitFile.Name -NoExtraLines }
            'Failed' { Invoke-Output -Type Missing -Message "Failed: " -TextMaker "$($fitFile.Name) - $message" -NoExtraLines }
        }
        Write-Log -Message " >> $status : $($fitFile.FullName) $message"

        [pscustomobject]@{
            File    = $fitFile.Name
            Status  = $status
            Message = $message
        }
    }

    $uploadedCount = @($results | Where-Object Status -eq 'Uploaded').Count
    $duplicateCount = @($results | Where-Object Status -eq 'Duplicate').Count
    $failedCount = @($results | Where-Object Status -eq 'Failed').Count

    Write-Host
    Invoke-Output -Type Bullet -Message "Uploaded: " -TextMaker $uploadedCount -NoExtraLines
    Invoke-Output -Type Bullet -Message "Duplicates: " -TextMaker $duplicateCount -NoExtraLines
    Invoke-Output -Type Bullet -Message "Failed: " -TextMaker $failedCount

    if ($failedCount -gt 0) {
        Invoke-Output -Type Warning -Message "$failedCount of $($fitFiles.Count) FIT files could not be uploaded."
    }
    else {
        Invoke-Output -Type Success -Message "FIT upload completed successfully."
    }
    ######################## main code ############################
    $runtime = Get-RunTime -StartRunTime $StartRunTime
    Write-Log -Message " Run Time: $runtime [h] ###"
    Write-Log -Message "### End Function $CurrentFunction ###"

    return $results
}