add Upload-File function to Tools.ps1
This commit is contained in:
@@ -209,6 +209,225 @@ Function Download-File {
|
||||
Return $File
|
||||
}
|
||||
|
||||
# Upload a file to a LiquidFiles Filedrop and submit its notification message.
|
||||
Function Upload-File {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Uploads a file to the Emberkom LiquidFiles Filedrop.
|
||||
.DESCRIPTION
|
||||
Uses the LiquidFiles 3.7+ JSON API with bounded binary chunks, including in
|
||||
32-bit Windows PowerShell 5. The local file is retained. Returns a receipt
|
||||
only after the Filedrop accepts the final message for delivery.
|
||||
|
||||
Uploads run synchronously. Allow enough time in the RMM for the entire
|
||||
transfer; the temporary Filedrop API key expires after 24 hours. Failed
|
||||
chunks are retried within this call, but restarting the script starts a
|
||||
new transfer. Final message submission is not retried automatically,
|
||||
because a lost response could otherwise cause duplicate notifications.
|
||||
.PARAMETER Path
|
||||
Literal path of one completed file. Also accepts -File or a pipeline path.
|
||||
.PARAMETER From
|
||||
Optional sender override. Defaults to the computer's fully qualified host
|
||||
name at ek-upload.net (or its host name when no DNS suffix is configured).
|
||||
.PARAMETER ChunkSizeMB
|
||||
Maximum upload buffer size in MiB; defaults to 10 regardless of file size.
|
||||
.PARAMETER TimeoutSeconds
|
||||
Timeout for each HTTP request, not for the entire transfer.
|
||||
.EXAMPLE
|
||||
Upload-File -Path $DsaResultPath
|
||||
.EXAMPLE
|
||||
Upload-File -File 'C:\Reports\report.zip' -Subject 'Diagnostic report' -Verbose
|
||||
.LINK
|
||||
https://docs.liquidfiles.com/api/v4.3/filedrop/
|
||||
.LINK
|
||||
https://docs.liquidfiles.com/api/v4.3/attachments/chunks.html
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
|
||||
[Alias('File','FullName')][ValidateNotNullOrEmpty()][string]$Path,
|
||||
[ValidateNotNullOrEmpty()][string]$FileDropUrl = 'https://xfer.emberkom.com/filedrop/cmd',
|
||||
[string]$From,
|
||||
[string]$Subject,
|
||||
[string]$Message,
|
||||
[ValidateRange(1,100)][int]$ChunkSizeMB = 10,
|
||||
[ValidateRange(1,3600)][int]$TimeoutSeconds = 900,
|
||||
[ValidateRange(0,10)][int]$RetryCount = 3
|
||||
)
|
||||
PROCESS {
|
||||
$UploadUri = [uri]$FileDropUrl
|
||||
If ( !$UploadUri.IsAbsoluteUri -or $UploadUri.Scheme -ne 'https' -or
|
||||
$UploadUri.UserInfo -or $UploadUri.Query -or $UploadUri.Fragment ) {
|
||||
Throw 'FileDropUrl must be an absolute HTTPS Filedrop URL without credentials, query, or fragment.'
|
||||
}
|
||||
$UploadBaseUrl = $UploadUri.AbsoluteUri.TrimEnd('/')
|
||||
$UploadFile = Get-Item -LiteralPath $Path -Force -ErrorAction Stop
|
||||
If ( $UploadFile -isnot [System.IO.FileInfo] ) { Throw 'Upload-File requires a file, not a directory.' }
|
||||
|
||||
$UploadHostInfo = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
|
||||
$UploadHostName = $UploadHostInfo.HostName
|
||||
If ( [string]::IsNullOrWhiteSpace($UploadHostName) ) { $UploadHostName = [Environment]::MachineName }
|
||||
$UploadDnsSuffix = $UploadHostInfo.DomainName.Trim('.')
|
||||
If ( $UploadDnsSuffix -and !$UploadHostName.EndsWith(".${UploadDnsSuffix}", [StringComparison]::OrdinalIgnoreCase) ) {
|
||||
$UploadHostName = "${UploadHostName}.${UploadDnsSuffix}"
|
||||
}
|
||||
$UploadSender = $From
|
||||
If ( [string]::IsNullOrWhiteSpace($UploadSender) ) { $UploadSender = $UploadHostName.ToLowerInvariant() + '@ek-upload.net' }
|
||||
Try { $UploadSender = ([System.Net.Mail.MailAddress]::new($UploadSender)).Address }
|
||||
Catch { Throw 'From must be a valid sender email address.' }
|
||||
$UploadSubject = $Subject
|
||||
If ( [string]::IsNullOrWhiteSpace($UploadSubject) ) { $UploadSubject = "File upload from ${UploadHostName}: $($UploadFile.Name)" }
|
||||
$UploadMessage = $Message
|
||||
If ( [string]::IsNullOrWhiteSpace($UploadMessage) ) { $UploadMessage = "Uploaded by ${UploadHostName}." }
|
||||
|
||||
Add-Type -AssemblyName System.Net.Http -ErrorAction Stop
|
||||
$UploadClient = $null
|
||||
$UploadHandler = $null
|
||||
$UploadStream = $null
|
||||
$UploadApiKey = $null
|
||||
|
||||
# Keep the same HTTP client (and cookies) for every chunk in this session.
|
||||
Function Invoke-FileDropRequest {
|
||||
param(
|
||||
[string]$Method,
|
||||
[string]$Uri,
|
||||
[byte[]]$Data,
|
||||
[int]$Count = 0,
|
||||
[string]$ContentType,
|
||||
[int]$Retries = $RetryCount
|
||||
)
|
||||
For ( $UploadAttempt = 0; $UploadAttempt -le $Retries; $UploadAttempt++ ) {
|
||||
$UploadRequest = $null
|
||||
$UploadResponse = $null
|
||||
$UploadStatus = 0
|
||||
Try {
|
||||
$UploadRequest = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $Uri)
|
||||
If ( $Method -eq 'POST' ) {
|
||||
$UploadRequest.Content = [System.Net.Http.ByteArrayContent]::new($Data, 0, $Count)
|
||||
If ( $ContentType ) {
|
||||
$UploadRequest.Content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse($ContentType)
|
||||
}
|
||||
}
|
||||
$UploadResponse = $UploadClient.SendAsync($UploadRequest).GetAwaiter().GetResult()
|
||||
$UploadStatus = [int]$UploadResponse.StatusCode
|
||||
$UploadResponseText = $UploadResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||||
If ( !$UploadResponse.IsSuccessStatusCode ) {
|
||||
# Do not include authentication tokens in errors or RMM logs.
|
||||
If ( $UploadApiKey ) { $UploadResponseText = $UploadResponseText.Replace($UploadApiKey, '[redacted]') }
|
||||
If ( $UploadResponseText.Length -gt 1000 ) { $UploadResponseText = $UploadResponseText.Substring(0, 1000) }
|
||||
Throw "Filedrop returned HTTP ${UploadStatus}: ${UploadResponseText}"
|
||||
}
|
||||
If ( [string]::IsNullOrWhiteSpace($UploadResponseText) ) { Return $null }
|
||||
$UploadResponseData = ConvertFrom-Json -InputObject $UploadResponseText -ErrorAction Stop
|
||||
If ( $UploadResponseData.errors ) { Throw ('Filedrop rejected the request: ' + ($UploadResponseData.errors -join '; ')) }
|
||||
Return $UploadResponseData
|
||||
}
|
||||
Catch {
|
||||
$UploadCanRetry = $UploadStatus -eq 0 -or $UploadStatus -in @(408,429,500,502,503,504)
|
||||
If ( !$UploadCanRetry -or $UploadAttempt -ge $Retries ) { Throw }
|
||||
Write-Verbose "Retrying Filedrop request after a connection error or HTTP ${UploadStatus}."
|
||||
}
|
||||
Finally {
|
||||
If ( $null -ne $UploadResponse ) { $UploadResponse.Dispose() }
|
||||
If ( $null -ne $UploadRequest ) { $UploadRequest.Dispose() }
|
||||
}
|
||||
Start-Sleep -Seconds ([Math]::Min(30, [Math]::Pow(2, $UploadAttempt + 1)))
|
||||
}
|
||||
}
|
||||
|
||||
Try {
|
||||
# Deny writes while reading so retried chunks always contain the same bytes.
|
||||
$UploadStream = [System.IO.File]::Open($UploadFile.FullName, [System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)
|
||||
[long]$UploadLength = $UploadStream.Length
|
||||
$UploadHandler = [System.Net.Http.HttpClientHandler]::new()
|
||||
$UploadHandler.AllowAutoRedirect = $false
|
||||
$UploadClient = New-Object -TypeName System.Net.Http.HttpClient -ArgumentList $UploadHandler
|
||||
$UploadClient.Timeout = [TimeSpan]::FromSeconds($TimeoutSeconds)
|
||||
$UploadClient.DefaultRequestHeaders.Accept.ParseAdd('application/json')
|
||||
|
||||
$UploadInfo = (Invoke-FileDropRequest -Method GET -Uri $UploadBaseUrl).filedrop
|
||||
$UploadApiKey = [string]$UploadInfo.api_key
|
||||
If ( [string]::IsNullOrWhiteSpace($UploadApiKey) ) {
|
||||
Throw 'The Filedrop did not provide an API key. Check its URL, password, and email-validation requirements.'
|
||||
}
|
||||
If ( $UploadInfo.max_upload_size -gt 0 -and $UploadLength -gt ([long]$UploadInfo.max_upload_size * 1MB) ) {
|
||||
Throw "The file exceeds the Filedrop limit of $($UploadInfo.max_upload_size) MiB."
|
||||
}
|
||||
$UploadExtension = $UploadFile.Extension.TrimStart('.').ToLowerInvariant()
|
||||
$UploadPermitted = $UploadInfo.permitted_extensions
|
||||
If ( !$UploadPermitted ) { $UploadPermitted = $UploadInfo.limited_extensions }
|
||||
$UploadAllowedExtensions = @(([string]$UploadPermitted -split '[,\s]+') | ForEach-Object { $_.TrimStart('.') } | Where-Object { $_ })
|
||||
$UploadBlockedExtensions = @(([string]$UploadInfo.blocked_extensions -split '[,\s]+') | ForEach-Object { $_.TrimStart('.') } | Where-Object { $_ })
|
||||
If ( ($UploadAllowedExtensions.Count -gt 0 -and $UploadExtension -notin $UploadAllowedExtensions) -or
|
||||
$UploadExtension -in $UploadBlockedExtensions ) {
|
||||
Throw "The Filedrop does not permit the file extension '$UploadExtension'."
|
||||
}
|
||||
$UploadAuth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($UploadApiKey + ':x'))
|
||||
$UploadClient.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Basic', $UploadAuth)
|
||||
|
||||
[int]$UploadChunkSize = $ChunkSizeMB * 1MB
|
||||
[long]$UploadChunks = [Math]::Max(1, [Math]::Ceiling($UploadLength / [double]$UploadChunkSize))
|
||||
$UploadBuffer = [byte[]]::new([int][Math]::Min($UploadChunkSize, [Math]::Max(1, $UploadLength)))
|
||||
$UploadEncodedName = [uri]::EscapeDataString($UploadFile.Name)
|
||||
$UploadAttachment = $null
|
||||
Write-Verbose "Uploading $($UploadFile.Name) ($UploadLength bytes) in $UploadChunks chunk(s) as ${UploadSender}."
|
||||
For ( [long]$UploadChunk = 0; $UploadChunk -lt $UploadChunks; $UploadChunk++ ) {
|
||||
[int]$UploadBytesNeeded = [Math]::Min($UploadChunkSize, $UploadLength - $UploadStream.Position)
|
||||
[int]$UploadBytesRead = 0
|
||||
While ( $UploadBytesRead -lt $UploadBytesNeeded ) {
|
||||
$UploadRead = $UploadStream.Read($UploadBuffer, $UploadBytesRead, $UploadBytesNeeded - $UploadBytesRead)
|
||||
If ( $UploadRead -eq 0 ) { Throw 'The local file ended before all expected bytes were read.' }
|
||||
$UploadBytesRead += $UploadRead
|
||||
}
|
||||
$UploadChunkUrl = "${UploadBaseUrl}/attachments/upload?filename=${UploadEncodedName}&chunk=${UploadChunk}&chunks=${UploadChunks}"
|
||||
$UploadChunkResult = Invoke-FileDropRequest -Method POST -Uri $UploadChunkUrl -Data $UploadBuffer -Count $UploadBytesRead
|
||||
If ( $UploadChunkResult.attachment.id ) { $UploadAttachment = $UploadChunkResult.attachment }
|
||||
Write-Progress -Activity "Uploading $($UploadFile.Name)" -Status "Chunk $($UploadChunk + 1) of ${UploadChunks}" `
|
||||
-PercentComplete ([int](100 * ($UploadChunk + 1) / $UploadChunks))
|
||||
}
|
||||
If ( !$UploadAttachment.id ) { Throw 'The upload did not return an attachment ID; the Filedrop message was not submitted.' }
|
||||
If ( $null -ne $UploadAttachment.size -and [long]$UploadAttachment.size -ne $UploadLength ) {
|
||||
Throw 'The uploaded attachment size does not match the local file; the Filedrop message was not submitted.'
|
||||
}
|
||||
If ( $UploadAttachment.content_blocked ) { Throw 'LiquidFiles blocked this attachment; the Filedrop message was not submitted.' }
|
||||
|
||||
$UploadBody = @{ message = @{
|
||||
from = $UploadSender
|
||||
subject = $UploadSubject
|
||||
message = $UploadMessage
|
||||
attachments = @([string]$UploadAttachment.id)
|
||||
} } | ConvertTo-Json -Depth 4 -Compress
|
||||
$UploadBodyBytes = [Text.Encoding]::UTF8.GetBytes($UploadBody)
|
||||
Try {
|
||||
$UploadReceipt = Invoke-FileDropRequest -Method POST -Uri $UploadBaseUrl -Data $UploadBodyBytes `
|
||||
-Count $UploadBodyBytes.Length -ContentType 'application/json; charset=utf-8' -Retries 0
|
||||
If ( [string]::IsNullOrWhiteSpace([string]$UploadReceipt.message.status) ) {
|
||||
Throw 'The server did not return a Filedrop submission confirmation.'
|
||||
}
|
||||
}
|
||||
Catch {
|
||||
Throw "Filedrop submission was not confirmed. Check the Filedrop before retrying to avoid duplicate notifications. $($_.Exception.Message)"
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Path = $UploadFile.FullName
|
||||
FileDropUrl = $UploadBaseUrl
|
||||
From = $UploadSender
|
||||
AttachmentId = [string]$UploadAttachment.id
|
||||
Size = $UploadLength
|
||||
Status = [string]$UploadReceipt.message.status
|
||||
}
|
||||
}
|
||||
Finally {
|
||||
Write-Progress -Activity "Uploading $($UploadFile.Name)" -Completed
|
||||
If ( $null -ne $UploadStream ) { $UploadStream.Dispose() }
|
||||
If ( $null -ne $UploadClient ) { $UploadClient.Dispose() }
|
||||
ElseIf ( $null -ne $UploadHandler ) { $UploadHandler.Dispose() }
|
||||
$UploadApiKey = $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Install a program from a provided path
|
||||
Function Install-Program {
|
||||
param(
|
||||
|
||||
Reference in New Issue
Block a user