I'm trying to upload a zip file to Nexus repository after converting the zipe file (1.6gb) to bytes.
The problem is when it readallbytes of the zip file, if the file size is big it throw system.outofmemory error. it doesn't throw the error if it's just 3-600mb, is there a way to read bytes and avoid the oufofmemory error?
Exception calling "ReadAllText" with "1" argument(s): "Exception of type 'System.OutOfMemoryException' was thrown."
$fileBin = [System.IO.File]::ReadAlltext($File)
Function Upload-File-To-Nexus {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True )][ValidateNotNullOrEmpty()][string]$File, # aka File
[parameter(Mandatory = $True )][ValidateNotNullOrEmpty()][string]$Repository,
[parameter(Mandatory = $True )][ValidateNotNullOrEmpty()][string]$Directory,
[Parameter(Mandatory = $False)][ValidateNotNullOrEmpty()][string]$FileName # destination filename, which can be derived from uploaded file
)
Begin {
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("fahq-ra-build:tEDlCI=0m9CES8l*lk?b"))
$header = #{authorization = "Basic $token" }
$Uri = "https://nexus-arps.corp.firstam.com/service/rest/v1/components?repository=${Repository}"
$boundary = [System.Guid]::NewGuid().ToString()
$LF = "`r`n"
}
Process {
If (!($FileName -ne $Null -And $FileName -ne "")) {
# if filename is null, then just use the filename from the input file
$FileName = Split-Path $File -Leaf
}
Try { $fileBin = [System.IO.File]::ReadAlltext($File) }
Catch {
throw $_.exception
Throw "Unable to read file $File. Aborted."
}
$bodyLines = (
"--${boundary}",
"Content-Disposition: form-data; name=`"raw.directory`"",
"",
"${Directory}",
"--${boundary}",
"Content-Disposition: form-data; name=`"raw.asset1`"; filename=`"${FileName}`"",
"Content-Type: application/octet-stream",
"",
$file,
"",
"--${boundary}",
"Content-Disposition: form-data; name=`"raw.asset1.filename`"",
"",
"${FileName}",
"--${boundary}--",
""
) -join $LF
$Response = Invoke-WebRequest -Uri $Uri -Method "POST" -Headers $header -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines
If ($Response.StatusCode -ge 200 -And $Response.StatusCode -lt 300) {
$Output = "https://${Server}/repository/${repository}/${directory}/${FileName}" -Replace "//", "/"
Write-Output $Output
}
Else {
Write-Output $Response
}
}
}
You probably meant .ReadAllBytes(), given that your'e not dealing with a text-based files.
However, you don't have to load the file into memory - use Invoke-WebRequest's -InFile parameter to specify a file to upload.
IIRC, the way ReadAllBytes() works is it doesn't know or lookup the file size up front. Instead, it guesses a buffer size for the resulting byte array. When the guess fails, it doubles the guess, allocates a new array, and copies what was read so far. This repeats until the file is loaded.
You get OutOfMemory, then, not only from actually running out of memory but also from running out address space in the current process from the extra buffer, where individual processes are limited by default to only 2GB.
If that sounds bad and inefficient, you're right, and RealAllBytes() is really only useful for small files.
When dealing with larger files, you need to use the streaming APIs, such as File.OpenRead(). Here you can check the actual size of the file, use that to allocate the exact size byte array, and then read in a small chunk (say, 4K) at a time.
Even better, since you're already using Invoke-WebRequest did you know it supports a -InFile argument, which can be used to upload a file without loading the entire thing into memory?
Related
I have this powershell script that uploads a file :
$URL = 'DESTINATION';
function UploadFile {
param (
$path,
$filename
)
$LF = "`r`n";
$fileBytes = [System.IO.File]::ReadAllBytes($path);
$fileEnc = [System.Text.Encoding]::GetEncoding('UTF-8').GetString($fileBytes);
$boundary = [System.Guid]::NewGuid().ToString();
$bodyLines = (
"--$boundary",
"Content-Disposition: form-data; filename=`"$filename`"",
"Content-Type: multipart/form-data$LF",
$fileEnc,
"--$boundary--$LF"
) -join $LF;
Invoke-RestMethod -Uri $URL -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines;
}
And my file is a .sqlite database.
The issue is that even if the file is actually uploaded, the .sqlite file is corrupted and can't be read with DB Browser for example. I precise that the original file before upload is not corrupted.
I presume there must be something wrong with the readfile/upload process, but I can't see what is wrong. Can someone please help me?
$API_KEY = "xxxxxxxxxx"
# Source image files
$ImageFiles = (Get-ChildItem -Path C:\Users\sam\Desktop\jpeg\* -filter *).Name
$uploadedFiles = #()
try {
foreach ($imageFile in $ImageFiles ) {
# 1a. RETRIEVE THE PRESIGNED URL TO UPLOAD THE FILE.
# Prepare URL for `Get Presigned URL` API call
$query = "https://api.pdf.co/v1/file/upload/get-presigned-url?
contenttype=application/octet-stream&name=" + `
[IO.Path]::GetFileName($imageFile)
$query = [System.Uri]::EscapeUriString($query)
# Execute request
$jsonResponse = Invoke-RestMethod -Method Get -Headers #{ "x-api-key" = $API_KEY } -Uri
$query
if ($jsonResponse.error -eq $false) {
# Get URL to use for the file upload
$uploadUrl = $jsonResponse.presignedUrl
# Get URL of uploaded file to use with later API calls
$uploadedFileUrl = $jsonResponse.url
# 1b. UPLOAD THE FILE TO CLOUD.
$r = Invoke-WebRequest -Method Put -Headers #{ "x-api-key" = $API_KEY; "content-type"
= "application/octet-stream" } -InFile $imageFile -Uri $uploadUrl
if ($r.StatusCode -eq 200) {
# Keep uploaded file URL
$uploadedFiles += $uploadedFileUrl
}
else {
# Display request error status
Write-Host $r.StatusCode + " " + $r.StatusDescription
}
}
else {
# Display service reported error
Write-Host $jsonResponse.message
}
}
if ($uploadedFiles.length -gt 0) {
# 2. CREATE PDF DOCUMENT FROM UPLOADED IMAGE FILES
# Prepare URL for `DOC To PDF` API call
$query = "https://api.pdf.co/v1/pdf/convert/from/image"
# Prepare request body (will be auto-converted to JSON by Invoke-RestMethod)
# See documentation: https://apidocs.pdf.co
$body = #{
"name" = $(Split-Path $DestinationFile -Leaf)
"url" = $uploadedFiles -join ","
} | ConvertTo-Json
# Execute request
$response = Invoke-WebRequest -Method Post -Headers #{ "x-api-key" = $API_KEY; "Content-
Type" = "application/json" } -Body $body -Uri $query
$jsonResponse = $response.Content | ConvertFrom-Json
if ($jsonResponse.error -eq $false) {
# Get URL of generated PDF file
$resultFileUrl = $jsonResponse.url;
$DestinationFile = "C:\Users\sam\Desktop\pdf\$imagefile.split('.')[0]"
# Download PDF file
Invoke-WebRequest -Headers #{ "x-api-key" = $API_KEY } -OutFile $DestinationFile -Uri
$resultFileUrl
Write-Host "Generated PDF file saved as `"$($DestinationFile)`" file."
}
else {
# Display service reported error
Write-Host $jsonResponse.message
}
}
}
catch {
# Display request error
Write-Host $_.Exception
}
Basically this script converts bulk number of JPEG images to PDF format .Its working initially but later when i execute the script it is getting failed by this error "The underlying connection was closed: An unexpected error occurred on a send." . I googled this issue and added these two lines at the beginning of the script –
#[Net.ServicePointManager]::SecurityProtocolNet.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 .
Even after adding these two am getting the same old error .Can anyone please help me with this issue
I'm trying to send a file content to server:
$uri = ...
$headers = #{
...
"Content-Type" = "application/json"
}
[string] $content = Get-Content .\filename -Encoding utf8 -Raw
$body = #{
...
"content" = $content
} | ConvertTo-Json
$response = Invoke-WebRequest $uri -Method 'PUT' -Headers $headers -Body $body
But all of non-ascii symbols are changed to another similar symbols or question marks
How can I escape them?
I've read documentation and I know about parameter -EscapeHandling of cmdlet ConvertTo-Json, but it's available from PowerShell 6.2, I have only 5.1
As a result, I wrote a simple function:
function EscapeNonAscii([string] $s)
{
$sb = New-Object System.Text.StringBuilder;
for ([int] $i = 0; $i -lt $s.Length; $i++)
{
$c = $s[$i];
if ($c -gt 127)
{
$sb = $sb.Append("\u").Append(([int] $c).ToString("X").PadLeft(4, "0"));
}
else
{
$sb = $sb.Append($c);
}
}
return $sb.ToString();
}
And used it like this:
$updateFileResponse = Invoke-WebRequest $updateFileUri -Method 'PUT' -Headers $headers -Body (EscapeNonAscii $body)
It helped. For everybody who will google it in the future, it's a request to GitLab API Update existing file in repository
PS: I use PS as C# because I know it badly. If somebody knows how to rewrite this fragment better please let me know.
PPS: And also I know that StringBuilder.Append changes an existing object, but I add here assigning ($sb = $sb.Append($c) instead of simple $sb.Append($c)) because it prints every action to console. If you know how to fix it please let me know.
I've been trying to build out some code in PowerShell (PowerShell 6 Core works, the following code didn't work in PowerShell 5),
$Token = ' Token Here ';
$Headers = #{
Method = 'POST'
Uri = ' URL Here '
Headers = #{Authorization = "Bearer $Token" }
}
$bodyHere = #{
roomId = ' ChatSpace ID Here '
text = ' Some Random Text '
files = Get-Item -Path 'c:\test.png'
}
try {
Invoke-RestMethod #Headers -Form $bodyHere
} catch [System.Net.WebException] {
Write-Error $_.Exception.ToString()
throw $_
}
This works well and can upload the file, however I need to also add Content-Type: "image/png" to my Get-Item item - is there an easy way to do this?
I've also trying to do it another way by building a multipart form I've seen someone else use on StackOverflow, but I get another issue now where I can't add to the multipart form when I attempt to use Add-Member or use any other method to append to the form.
$Token = ' Token Here ';
$Headers = #{
Method = 'POST'
Uri = ' URL Here '
Headers = #{Authorization = "Bearer $Token" }
}
$bodyLines = #{
roomId = ' ChatSpace ID Here '
text = ' Random Text here '
}
$FilePath = 'c:\test.png'
$FieldName = 'files'
$ContentType = 'image/png'
$FileStream = [System.IO.FileStream]::New($filePath, [System.IO.FileMode]::Open)
$FileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::New('form-data')
$FileHeader.Name = $FieldName
$FileHeader.FileName = Split-Path -Leaf $FilePath
$FileContent = [System.Net.Http.StreamContent]::New($FileStream)
$FileContent.Headers.ContentDisposition = $FileHeader
$FileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse($ContentType)
$MultipartContent = [System.Net.Http.MultipartFormDataContent]::New()
$MultipartContent.Add($FileContent)
$MultipartContent.Add($bodyLines) # this doesn't work as I'm building a multipart form
$MultipartContent | Add-Member -Name "headers" -Value $bodyLines -MemberType NoteProperty # I convert this to JSON and use write-host but it doesn't append I've also tried $MultipartContent.headers and it still doesn't append
try {
# Invoke-RestMethod #Headers -Body $MultipartContent
Invoke-WebRequest #Headers -Body $bodyLines
} catch [System.Net.WebException] {
Write-Error $_.Exception.ToString()
throw $_
}
Any help on either how to build a multipart form with both a file upload and other parameters or tacking on content-type to a Get-Item call, would be super appreciated. Just to give you an idea of what the code looks like in Python (much easier):
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
m = MultipartEncoder({'roomId': ' ChatSpace ID Here ',
'text': ' Random Text Here ',
'files': ('example.png', open('example.png', 'rb'),
'image/png')})
r = requests.post(' URL Here ', data=m,
headers={'Authorization': 'Bearer TokenHere',
'Content-Type': m.content_type})
print r.text
I think you want the content of the file instead of the file information. Try something like files = (Get-Content 'c:\test.png') in your script. If you just want the path to file you don't need to use Get-Item at all.
If you are uploading a .PNG file this may not work if the bytes include control characters that fool the server side parsing of the input.
I'm trying to send a file via Invoke-RestMethod in a similar context as curl with the -F switch.
Curl Example
curl -F FileName=#"/path-to-file.name" "https://uri-to-post"
In powershell, I've tried something like this:
$uri = "https://uri-to-post"
$contentType = "multipart/form-data"
$body = #{
"FileName" = Get-Content($filePath) -Raw
}
Invoke-WebRequest -Uri $uri -Method Post -ContentType $contentType -Body $body
}
If I check fiddler I see that the body contains the raw binary data, but I get a 200 response back showing no payload has been sent.
I've also tried to use the -InFile parameter with no luck.
I've seen a number of examples using a .net class, but was trying to keep this simple with the newer Powershell 3 commands.
Does anyone have any guidance or experience making this work?
The accepted answer won't do a multipart/form-data request, but rather a application/x-www-form-urlencoded request forcing the Content-Type header to a value that the body does not contain.
One way to send a multipart/form-data formatted request with PowerShell is:
$ErrorActionPreference = 'Stop'
$fieldName = 'file'
$filePath = 'C:\Temp\test.pdf'
$url = 'http://posttestserver.com/post.php'
Try {
Add-Type -AssemblyName 'System.Net.Http'
$client = New-Object System.Net.Http.HttpClient
$content = New-Object System.Net.Http.MultipartFormDataContent
$fileStream = [System.IO.File]::OpenRead($filePath)
$fileName = [System.IO.Path]::GetFileName($filePath)
$fileContent = New-Object System.Net.Http.StreamContent($fileStream)
$content.Add($fileContent, $fieldName, $fileName)
$result = $client.PostAsync($url, $content).Result
$result.EnsureSuccessStatusCode()
}
Catch {
Write-Error $_
exit 1
}
Finally {
if ($client -ne $null) { $client.Dispose() }
if ($content -ne $null) { $content.Dispose() }
if ($fileStream -ne $null) { $fileStream.Dispose() }
if ($fileContent -ne $null) { $fileContent.Dispose() }
}
The problem here was what the API required some additional parameters. Initial request required some parameters to accept raw content and specify filename/size. After setting that and getting back proper link to submit, I was able to use:
Invoke-RestMethod -Uri $uri -Method Post -InFile $filePath -ContentType "multipart/form-data"
I found this post and changed it a bit
$fileName = "..."
$uri = "..."
$currentPath = Convert-Path .
$filePath="$currentPath\$fileName"
$fileBin = [System.IO.File]::ReadAlltext($filePath)
$boundary = [System.Guid]::NewGuid().ToString()
$LF = "`r`n"
$bodyLines = (
"--$boundary",
"Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"",
"Content-Type: application/octet-stream$LF",
$fileBin,
"--$boundary--$LF"
) -join $LF
Invoke-RestMethod -Uri $uri -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines
For anyone wondering (like Jelphy) whether David's answer can be used with cookies/credentials, the answer is yes.
First set the session with Invoke-WebRequest:
Invoke-WebRequest -Uri "$LoginUri" -Method Get -SessionVariable 'Session'
Then POST to the Login URL, which stores the authentication cookie in $Session:
$Response = Invoke-WebRequest -Uri "$Uri" -Method Post -Body $Body -WebSession $Session
The steps above are the standard way to deal with session in Powershell. But here is the important part. Before creating the HttpClient, create an HttpClientHandler and set it's CookieContainer property with the cookies from the session:
$ClientMessageHandler = New-Object System.Net.Http.HttpClientHandler
$ClientMessageHandler.CookieContainer = $Session.Cookies
Then pass this object to the HttpClient constructor
$Client = [System.Net.Http.HttpClient]::new($ClientMessageHandler)
Voila, you now have an HttpClient with session cookies set automatically via Invoke-WebRequest. The rest of David's example should work (copied here for completeness):
$MultipartFormData = New-Object System.Net.Http.MultipartFormDataContent
$FileStream = [System.IO.File]::OpenRead($FilePath)
$FileName = [System.IO.Path]::GetFileName($FilePath)
$FileContent = New-Object System.Net.Http.StreamContent($FileStream)
$MultipartFormData.Add($FileContent, $FieldName, $FileName)
$Result = $Client.PostAsync($url, $content).Result
$Result.EnsureSuccessStatusCode()
I had many files to upload with each request, so I factored out this last bit into a lambda function:
function Add-FormFile {
param ([string]$Path, [string]$Name)
if ($Path -ne "")
{
$FileStream = [System.IO.File]::OpenRead($Path)
$FileName = [System.IO.Path]::GetFileName($Path)
$FileContent = [System.Net.Http.StreamContent]::new($FileStream)
$MultipartFormData.Add($FileContent, $Name, $FileName)
}
}