Powershell read file in chunks - powershell

I've had a script written in Powershell which transferred a file via FTP which worked fine by using:
$content = [System.IO.File]::ReadAllBytes($backup_app_data)
But this stopped working once the file size reached 2Gb, by throwing an error saying that this method is limited to reading files up to this size.
Exception calling "ReadAllBytes" with "1" argument(s): "The file is
too long. This operation is currently limited to supporting files less
than 2 gigabytes in size.
Now, I'm trying to modify my script and use an alternate approach with Read, which from what I understand will allow me to read the file in chunks and then write those chunks and so on.
Unfortunately although the script I've modified seems to be working, since a file is created on my FTP location, there is no data being transferred and no error is thrown by the script.
There is just a 0kb file created in the destination folder and the script ends.
I've tried to do some debugging, but I can't seem to find the issue. Below is the code I'm currently using.
$file = 'G:\Backups\DATA_backup_2016_09_05.zip'
$dest_name = 'DATA_backup_2016_09_05.zip'
$ftp = [System.Net.FtpWebRequest]::Create($ftp+$dest_name)
$ftp = [System.Net.FtpWebRequest]$ftp
$ftp.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$ftp.Credentials = new-object System.Net.NetworkCredential($user, $pass)
$ftp.UseBinary = $true
$ftp.UsePassive = $true
# determine the size of the file
$file_size = (Get-Item $file).length
$chunk_size = 512mb
$bytes_to_read = $file_size
$iterations = $file_size / $chunk_size
$iter = 0
$fstream = [System.IO.FileStream]
[byte[]] $byte_array
while ($bytes_to_read > 0){
if($iterations > 1) {
$content = $fstream.Read($byte_array, $iter, $chunk_size)
}
else {
$content = $fstream.Read($byte_array, $iter, $bytes_to_read)
}
$ftp.ContentLength = $content.Length
$rs = $ftp.GetRequestStream()
$rs.Write($content, 0, $content.Length)
# keep the loop going
$iter = $iter + 1
$iterations = $iterations - 1
$bytes_to_read = $bytes_to_read - $chunk_size
}
$rs.Close()
$rs.Dispose()
Any help is appreciated.

So, based on the suggestions in the comment section and using as an example the answer from the possible duplicate question, I managed to find the solution myself.
Below is the code I used to transfer via FTP a .zip archive which was 3.74Gb.
$ftp_addr = "ftp://ftp.somerandomwebsite.com/folder1/"
$user = "usr"
$pass = "passwrd"
$bufSize = 256mb
# ... missing code where I identify the file I want to FTP ... #
# Initialize connection to FTP
$ftp = [System.Net.FtpWebRequest]::Create($ftp_addr + $backup_file + ".zip")
$ftp = [System.Net.FtpWebRequest]$ftp
$ftp.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$ftp.Credentials = new-object System.Net.NetworkCredential($user, $pass)
$ftp.Timeout = -1 #infinite timeout
$ftp.ReadWriteTimeout = -1 #infinite timeout
$ftp.UseBinary = $true
$ftp.UsePassive = $true
$requestStream = $ftp.GetRequestStream()
$fileStream = [System.IO.File]::OpenRead($file_to_ftp)
$chunk = New-Object byte[] $bufSize
while ( $bytesRead = $fileStream.Read($chunk, 0, $bufSize) ){
$requestStream.write($chunk, 0, $bytesRead)
$requestStream.Flush()
}
$fileStream.Close()
$requestStream.Close()
So far, this code has worked flawlessly for about multiple (20+) attempts. I hope it helps others!

Related

How to download only a single file from an online ZIP archive via Powershell?

I want to download only a single file from an online ZIP archive via Powershell.
For this I created a demo-code which is already working, but I am still struggling to get the correct parsing logic on the ZIP-directory. Here is the code I have so far:
# demo code downloading a single DLL file from an online ZIP archive
# and extracting the DLL into memory for mounting it to the main process.
cls
Remove-Variable * -ea 0
# definition for the ZIP archive, the file to be extracted and the checksum:
$url = 'https://github.com/sshnet/SSH.NET/releases/download/2020.0.1/SSH.NET-2020.0.1-bin.zip'
$sub = 'net40/Renci.SshNet.dll'
$md5 = '5B1AF51340F333CD8A49376B13AFCF9C'
# prepare HTTP client:
Add-Type -AssemblyName System.Net.Http
$handler = [System.Net.Http.HttpClientHandler]::new()
$client = [System.Net.Http.HttpClient]::new($handler)
# get the length of the ZIP archive:
$req = [System.Net.HttpWebRequest]::Create($url)
$req.Method = 'HEAD'
$length = $req.GetResponse().ContentLength
$zip = [byte[]]::new($length)
# get the last 10k:
# how to get the correct length of the central ZIP directory here?
$start = $length-10kb
$end = $length-1
$client.DefaultRequestHeaders.Add('Range', "bytes=$start-$end")
$result = $client.GetAsync($url).Result
$last10kb = $result.content.ReadAsByteArrayAsync().Result
$last10kb.CopyTo($zip, $start)
# get the block containing the DLL file:
# how to get the exact file-offset from the ZIP directory?
$start = $length-3537kb
$end = $length-3201kb
$client.DefaultRequestHeaders.Clear()
$client.DefaultRequestHeaders.Add('Range', "bytes=$start-$end")
$result = $client.GetAsync($url).Result
$block = $result.content.ReadAsByteArrayAsync().Result
$block.CopyTo($zip, $start)
# extract the DLL file from archive:
Add-Type -AssemblyName System.IO.Compression
$stream = [System.IO.Memorystream]::new()
$stream.Write($zip,0,$zip.Length)
$archive = [System.IO.Compression.ZipArchive]::new($stream)
$entry = $archive.GetEntry($sub)
$bytes = [byte[]]::new($entry.Length)
[void]$entry.Open().Read($bytes, 0, $bytes.Length)
# check MD5:
$prov = [Security.Cryptography.MD5CryptoServiceProvider]::new().ComputeHash($bytes)
$hash = [string]::Concat($prov.foreach{$_.ToString("x2")})
if ($hash -ne $md5) {write-host 'dll has wrong checksum.' -f y ;break}
# load the DLL:
[void][System.Reflection.Assembly]::Load($bytes)
# use the single demo-call from the DLL:
$test = [Renci.SshNet.NoneAuthenticationMethod]::new('test')
'done.'
Only open point in this code is the correct method to identify the length of the central directory at the end of the ZIP archive and how to get the correct file-offset for the single file to be extracted (in my code I just found the ranges by pure try&error).
I already checked this wiki https://en.wikipedia.org/wiki/ZIP_(file_format)#Structure and also the PKWARE definitions https://gist.github.com/steakknife/820b73ebf25146180198febdb6f0e183 but beside the block definitions I could not find a programmatical approach to get the offset for ethe EOCD and the individual file. Can someone help here, please?
After a couple of additional tests I came to this solution:
# demo code downloading a single DLL file from an online ZIP archive
# and extracting the DLL into memory to mount it finally to the main process.
cls
Remove-Variable * -ea 0
# definition for the ZIP archive, the file to be extracted and the checksum:
$url = 'https://github.com/sshnet/SSH.NET/releases/download/2020.0.1/SSH.NET-2020.0.1-bin.zip'
$sub = 'net40/Renci.SshNet.dll'
$md5 = '5B1AF51340F333CD8A49376B13AFCF9C'
'prepare HTTP client:'
Add-Type -AssemblyName System.Net.Http
$handler = [System.Net.Http.HttpClientHandler]::new()
$client = [System.Net.Http.HttpClient]::new($handler)
'get the length of the ZIP archive:'
# dont use System.Web.HttpRequest, it is frequently hanging:
$req = [System.Net.Http.HttpRequestMessage]::new('HEAD', $url)
$result = $client.SendAsync($req).Result
$zipLength = $result.Content.Headers.ContentLength
$zip = [byte[]]::new($zipLength)
$req.Dispose()
'get the last 10k:'
$start = $zipLength-10kb
$end = $zipLength-1
$client.DefaultRequestHeaders.Add('Range', "bytes=$start-$end")
$result = $client.GetAsync($url).Result
$last10kb = $result.content.ReadAsByteArrayAsync().Result
$last10kb.CopyTo($zip, $start)
"get the 'End of CD' block:"
$enc = [System.Text.Encoding]::GetEncoding(28591)
$end = $enc.GetString($last10kb, $last10kb.Length-256, 256)
$eocd = [regex]::Match($end, 'PK\x05\x06.*').value
$eocd = $enc.GetBytes($eocd)
'get the central directory:'
$cdLength = [bitconverter]::ToUInt32($eocd, 12)
$cdStart = [bitconverter]::ToUInt32($eocd, 16)
$cd = [byte[]]::new($cdLength)
[array]::Copy($zip, $cdStart, $cd, 0, $cdLength)
'search all file headers for correct file name:'
$fileHeaders = [regex]::Split($enc.GetString($cd),'PK\x01\x02')
foreach ($header in $fileHeaders) {
$len = $header.Length
if ($len -ge 42) {
$bytes = $enc.GetBytes($header)
$nameLength = [bitconverter]::ToUInt16($bytes, 24)
if ($nameLength -eq $sub.length -and ($nameLength + 42) -le $len) {
$name = $header.Substring(42, $nameLength)
if ($name -eq $sub) {
$size = [bitconverter]::ToUInt32($bytes, 16) + 256
$start = [bitconverter]::ToUInt32($bytes, 38)
break
}
}
}
}
if (!$start) {write-host 'we could not find file in the ZIP archive' -f y ;break}
'get the block containing the file:'
$end = $start+$size
$client.DefaultRequestHeaders.Clear()
$client.DefaultRequestHeaders.Add('Range', "bytes=$start-$end")
$result = $client.GetAsync($url).Result
$block = $result.content.ReadAsByteArrayAsync().Result
$block.CopyTo($zip, $start)
$client.dispose()
'extract the DLL file from archive:'
Add-Type -AssemblyName System.IO.Compression
$stream = [System.IO.Memorystream]::new()
$stream.Write($zip,0,$zip.Length)
$archive = [System.IO.Compression.ZipArchive]::new($stream)
$entry = $archive.GetEntry($sub)
$bytes = [byte[]]::new($entry.Length)
[void]$entry.Open().Read($bytes, 0, $bytes.Length)
'check MD5:'
$prov = [Security.Cryptography.MD5CryptoServiceProvider]::new().ComputeHash($bytes)
$hash = [string]::Concat($prov.foreach{$_.ToString("x2")})
if ($hash -ne $md5) {write-host 'dll has wrong checksum.' -f y ;break}
'load the DLL:'
[void][System.Reflection.Assembly]::Load($bytes)
'use the single demo-call from the DLL:'
$test = [Renci.SshNet.NoneAuthenticationMethod]::new('test')
'done.'

Using PowerShell to download all files in a directory

I have a working PowerShell script that iterates through a directory accessed via FTP and prints it's contents. The script looks like this:
$sourceuri = "<string>"
$targetpath = "<string>"
$username = "<string>"
$password = "<string>"
# Create a FTPWebRequest object to handle the connection to the ftp server
$ftprequest = [System.Net.FtpWebRequest]::create($sourceuri)
# set the request's network credentials for an authenticated connection
$ftprequest.Credentials = New-Object System.Net.NetworkCredential($username,$password)
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails
$ftprequest.UseBinary = $true
$ftprequest.KeepAlive = $false
# send the ftp request to the server
$ftpresponse = $ftprequest.GetResponse()
$stream = $ftpresponse.getresponsestream()
$buffer = new-object System.Byte[] 1024
$encoding = new-object System.Text.AsciiEncoding
$outputBuffer = ""
$foundMore = $false
## Read all the data available from the stream, writing it to the
## output buffer when done.
do
{
## Allow data to buffer for a bit
start-sleep -m 1000
## Read what data is available
$foundmore = $false
$stream.ReadTimeout = 1000
do
{
try
{
$read = $stream.Read($buffer, 0, 1024)
if($read -gt 0)
{
$foundmore = $true
$outputBuffer += ($encoding.GetString($buffer, 0, $read))
}
}
catch
{
$foundMore = $false; $read = 0
}
}
while($read -gt 0)
}
while($foundmore)
$outputBuffer
My actual goal is not to just list the files but download them to the machine running the script. I'm finding this a bit tricky since my loops only reference bytes and not files by name. How can I use this loop to download all the files in a directory instead of just listing them?
I prefer use real FTP libraries like the one of winscp but there are other examples out there. http://winscp.net/eng/docs/library_session_listdirectory#example
once you have a the list of directories and files - use the get-content method to read from the list, and then use the bits transfer to download all files - I'm not sure if this will download the directories as well... but it will definitely download the files...

PowerShell upload file to ftp will upload file twice

UPDATE:
the
$ftprespsonse = [System.Net.FtpWebResponse]$ftp.GetResponse()
in the following code creates an empty file(file with the same name but has size 0), which leads to the duplication of my original question. My question is why the GetRepsonse creates that empty file? My guess right now is the [System.Net.FtpWebRequest]::Create and GetResponse will mess up stuff.
$username="user"
$password="pw"
$ftp = [System.Net.FtpWebRequest]::Create("ftp://xxx.xxx.xxx:{port}/file.txt")
$ftp = [System.Net.FtpWebRequest]$ftp
$ftp.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$ftp.Credentials = new-object System.Net.NetworkCredential($username,$password)
$ftp.UseBinary = $true
$ftp.UsePassive = $true
$ftp.EnableSsl = $true
$ftp.KeepAlive = $false
$ftprespsonse = [System.Net.FtpWebResponse]$ftp.GetResponse()
$content = [System.IO.File]::ReadAllBytes("c:\file.txt")
$ftp.ContentLength = $content.Length
try
{
$rs = $ftp.GetRequestStream()
$rs.Write($content, 0, $content.Length)
'File Uploaded.'
Write-Host 'Status code: ' + $ftprespsonse.StatusCode
Write-Host 'Status descriptionL: ' + $ftprespsonse.StatusDescription
$ftprespsonse.close()
$ftp.Abort()
$rs.Close()
$rs.Dispose()
}
catch [System.Exception]
{
'Upload failed.'
$ftprespsonse = [System.Net.FtpWebResponse]$ftp.GetResponse()
Write-Host 'Status code: ' + $ftprespsonse.StatusCode
Write-Host 'Status descriptionL: ' + $ftprespsonse.StatusDescription
$ftprespsonse.close()
$ftp.Abort()
}
By running this script, I can see the following output:
File Uploaded.
Status code: + ClosingData
Status descriptionL: + 226- Transfer complete - acknowledgment message is pending.
226- Transfer complete - acknowledgment message is pending.
226 Transfer complete (Batch Number = 30009).
And going to the remote folder, I can see that two files are created, the file names are the same, but one of them has file size 0 and the other is 570kb (which is correct).
any idea what happened?
Use Powershell FTP Module(http://gallery.technet.microsoft.com/scriptcenter/PowerShell-FTP-Client-db6fe0cb), you will sidestep having to implement your own FTP client and likely avoid your curious issue.
It turned out the order of
$ftp = [System.Net.FtpWebRequest]::Create($fileRemotePath)
and
$ftprespsonse = [System.Net.FtpWebResponse]$ftp.GetResponse()
and
$rs = $ftp.GetRequestStream()
messes up the stuff. By running the code line by line, I find that the empty file is created at the first appearance of the $ftp.GetResponse() , after that, the full file is created on line $ftp.GetRequestStream()
therefore, I will move the first GetResponse() to after the GetRequestStream()

Rename file on FTP with PowerShell

Is there a way to rename the file in a FTP directory?
I'm streaming live images from computer to FTP, but problem is that when it uploads the image to FTP it making instant replacement of a file. I want to firstly upload image with temporary name and then make a rename to live.jpg. It's gonna be like cached file uploading.
while($true)
{
$i++
$File = "c:\live\temp.jpg"
$ftp = "ftp://username:password#example.com/camera/temp.jpg"
$webclient = New-Object System.Net.WebClient
$uri = New-Object System.Uri($ftp)
$webclient.UploadFile($uri, $File)
}
How can I use this in script properly ?
Rename-Item ..\camera\temp.jpg live.jpg
Thanx!
Try this:
$ftp = [System.Net.FtpWebRequest]::Create("ftp://username:password#example.com/camera/temp.jpg")
$ftp.KeepAlive = $true
$ftp.UsePassive = $true
$ftp.Method = "Rename"
$ftp.RenameTo = "camera/temp1.jpg"
$ftp.UseBinary = $true
$response = [System.Net.FtpWebResponse] $ftp.GetResponse()
$ftp = [System.Net.FtpWebRequest]::Create("ftp://username:password#example.com/camera/temp.jpg")
$ftp.KeepAlive = $true
$ftp.UsePassive = $true
$ftp.Method = "Rename"
$ftp.RenameTo = "camera/temp1.jpg"
$ftp.UseBinary = $true
$response = [System.Net.FtpWebResponse] $ftp.GetResponse()
I had to adjust the $ftp.RenameTo line and remove the directory to make it work for me:
$ftp.RenameTo = "temp1.jpg"
Thanks anyway

Unable to read an open file with binary reader

I have this function to read the SQL Server errorlog but the problem is that I'm not able to read the errorlog that the server is using at the time. I have been google-ing and it seems that the Fileshare flag isn't working for powershell. Is there some way to set the the Fileshare flag when I try to open the file?
function check_logs{
param($logs)
$pos
foreach($log in $logpos){
if($log.host -eq $logs.host){
$currentLog = $log
break
}
}
if($currentLog -eq $null){
$currentLog = #{}
$logpos.Add($currentLog)
$currentLog.host = $logs.host
$currentLog.event = $logs.type
$currentLog.lastpos = 0
}
$path = $logs.file
if($currentLog.lastpos -ne $null){$pos = $currentLog.lastpos}
else{$pos = 0}
if($logs.enc -eq $null){$br = New-Object System.IO.BinaryReader([System.IO.File]::Open($path, [System.IO.FileMode]::Open))}
else{
$encoding = $logs.enc.toUpper().Replace('-','')
if($encoding -eq 'UTF16'){$encoding = 'Unicode'}
$br = New-Object System.IO.BinaryReader([System.IO.File]::Open($path, [System.IO.FileMode]::Open), [System.Text.Encoding]::$encoding)
}
$required = $br.BaseStream.Length - $pos
if($required -lt 0){
$pos = 0
$required = $br.BaseStream.Length
}
if($required -eq 0){$br.close(); return $null}
$br.BaseStream.Seek($pos, [System.IO.SeekOrigin]::Begin)|Out-Null
$bytes = $br.ReadBytes($required)
$result = [System.Text.Encoding]::Unicode.GetString($bytes)
$split = $result.Split("`n")
foreach($s in $split)
{
if($s.contains(" Error:"))
{
$errorLine = [regex]::Split($s, "\s\s+")
$err = [regex]::Split($errorLine[1], "\s+")
if(log_filter $currentLog.event $err[1..$err.length]){$Script:events = $events+ [string]$s + "`n" }
}
}
$currentLog.lastpos = $br.BaseStream.Position
$br.close()
}
To be clear the error comes when I try to open the file. The error message is:
Exception calling "Open" with "2" argument(s): "The process cannot access the file
'C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\Log\ERRORLOG'
because it is being used by another process."
Gísli
So I found the answer and it was pretty simple.
The binary reader constructor takes as input a stream. I didn't define the stream seperately and that's why I didn't notice that you set the FileShare flag in the stream's constructor.
What I had to do was to change this:
{$br = New-Object System.IO.BinaryReader([System.IO.File]::Open($path, [System.IO.FileMode]::Open))}
To this:
{$br = New-Object System.IO.BinaryReader([System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite))}
And then it worked like a charm.
Gísli