Differences
This shows you the differences between the selected revisions of the page.
| 2021-03-25 | 2021-03-25 | ||
| modernizing c# download code (martin) | full featured powershell version of the download code (martin) | ||
| Line 119: | Line 119: | ||
| ==== [[powershell]] PowerShell ==== | ==== [[powershell]] PowerShell ==== | ||
| - | The code is not equivalent to the C# example above. The PowerShell code does not download subdirectories. It also split the files to batches by their count only, instead of using a queue like the C# code. | + | The following code uses [[ps>threadjob/start-threadjob|''Start-ThreadJob'' cmdlet]] from ''ThreadJob'' module. It is a part of PowerShell 6 and newer. In PowerShell 5, it can be installed using ''Install-Module ThreadJob''. |
| <code powershell> | <code powershell> | ||
| param ( | param ( | ||
| $sessionUrl = "sftp://user:password;fingerprint=ssh-rsa-xxxxxxxxxxx...=@example.com/", | $sessionUrl = "sftp://user:password;fingerprint=ssh-rsa-xxxxxxxxxxx...=@example.com/", | ||
| - | $remotePath = "/home/user/", | + | $remotePath = "/remote/path/", |
| - | $localPath = "c:\downloaded\", | + | $localPath = "c:\local\path\", |
| $batches = 3 | $batches = 3 | ||
| ) | ) | ||
| + | · | ||
| try | try | ||
| { | { | ||
| - | $dllPath = (Join-Path $PSScriptRoot "WinSCPnet.dll") | + | $assemblyFilePath = "WinSCPnet.dll" |
| # Load WinSCP .NET assembly | # Load WinSCP .NET assembly | ||
| - | Add-Type -Path $dllPath | + | Add-Type -Path $assemblyFilePath |
| + | · | ||
| # Setup session options | # Setup session options | ||
| $sessionOptions = New-Object WinSCP.SessionOptions | $sessionOptions = New-Object WinSCP.SessionOptions | ||
| $sessionOptions.ParseUrl($sessionUrl) | $sessionOptions.ParseUrl($sessionUrl) | ||
| + | · | ||
| $started = Get-Date | $started = Get-Date | ||
| + | # Plain variables cannot be modified in job threads | ||
| + | $stats = @{ | ||
| + | count = 0 | ||
| + | bytes = [long]0 | ||
| + | } | ||
| + | · | ||
| try | try | ||
| { | { | ||
| Line 148: | Line 153: | ||
| $session.Open($sessionOptions) | $session.Open($sessionOptions) | ||
| - | # Retrieve list of files and sort them from larges to smallest | + | Write-Host "Starting files enumeration..." |
| - | [array]$files = | + | $files = |
| - | $session.ListDirectory($remotePath).Files | | + | $session.EnumerateRemoteFiles( |
| - | ···········Where-Object { -Not $_.IsDirectory } | | + | $remotePath, $Null, [WinSCP.EnumerationOptions]::AllDirectories) |
| - | Sort-Object Length -Descending | + | # An explicit implementation of IEnumerable cannot be accessed directly in PowerShell |
| + | ········$filesEnumerator = | ||
| + | [System.Collections.IEnumerable].GetMethod("GetEnumerator").Invoke($files, $Null) | ||
| + | |||
| + | ········for ($i = 1; $i -le $batches; $i++) | ||
| + | { | ||
| + | Start-ThreadJob -Name "Batch $i" -ArgumentList $i { | ||
| + | param ($no) | ||
| - | ········# Calculate total size of all files | + | ················try |
| - | ·······$total = ($files | Measure-Object -Property Length -Sum).Sum | + | ···············{ |
| - | + | ···················Write-Host "Starting download $no..." | |
| - | # And batch size | + | |
| - | ········$batch = [int]($total / $batches) | + | |
| - | ········Write-Host ( | + | ····················$downloadSession = New-Object WinSCP.Session |
| - | ···········"Will download $($files.Count) files totaling $total bytes in " + | + | ···················$downloadSession.Open($using:sessionOptions) |
| - | ···········"$batches parallel batches, $batch bytes on average in each") | + | |
| - | + | ||
| - | $start = 0 | + | |
| - | $sum = 0 | + | |
| - | $no = 0 | + | |
| - | ········for ($i = 0; $i -lt $files.Count; $i++) | + | ····················while ($True) |
| - | { | + | |
| - | $sum += $files[$i].Length | + | |
| - | + | ||
| - | # Found enough files for the next batch | + | |
| - | if (($sum -ge $batch) -or ($i -eq $files.Count - 1)) | + | |
| - | { | + | |
| - | Write-Host "Starting batch $no to download $($i - $start + 1) files totaling $sum" | + | |
| - | + | ||
| - | $fileList = $files[$start..$i] -join ";" | + | |
| - | + | ||
| - | # Start the background job for the batch | + | |
| - | Start-Job -Name "Batch $no" ` | + | |
| - | -ArgumentList $dllPath, $sessionUrl, $remotePath, $localPath, $no, $fileList { | + | |
| - | param ( | + | |
| - | [Parameter(Position = 0)] | + | |
| - | $dllPath, | + | |
| - | [Parameter(Position = 1)] | + | |
| - | $sessionUrl, | + | |
| - | [Parameter(Position = 2)] | + | |
| - | $remotePath, | + | |
| - | [Parameter(Position = 3)] | + | |
| - | $localPath, | + | |
| - | [Parameter(Position = 4)] | + | |
| - | $no, | + | |
| - | [Parameter(Position = 5)] | + | |
| - | $fileList | + | |
| - | ) | + | |
| - | + | ||
| - | try | + | |
| { | { | ||
| - | Write-Host "Starting batch $no" | + | [System.Threading.Monitor]::Enter($using:filesEnumerator) |
| - | + | ||
| - | # Load WinSCP .NET assembly. | + | |
| - | ························# Need to use an absolute path as the Job is started | + | |
| - | ························# from user's documents folder. | + | |
| - | Add-Type -Path $dllPath | + | |
| - | + | ||
| - | # Setup session options | + | |
| - | $sessionOptions = New-Object WinSCP.SessionOptions | + | |
| - | $sessionOptions.ParseUrl($sessionUrl) | + | |
| - | ························ | + | |
| try | try | ||
| { | { | ||
| - | Write-Host "Connecting batch $no..." | + | if (!($using:filesEnumerator).MoveNext()) |
| - | $session = New-Object WinSCP.Session | + | |
| - | + | ||
| - | $session.Open($sessionOptions) | + | |
| - | + | ||
| - | $files = $fileList -split ";" | + | |
| - | + | ||
| - | # Download the files selected for this batch | + | |
| - | foreach ($file in $files) | + | |
| { | { | ||
| - | $remoteFilePath = "$remotePath/$file" | + | break |
| - | $localFilePath = "$localPath\$file" | + | ···························} |
| - | Write-Host "Downloading $remoteFilePath to $localFilePath in $no" | + | |
| - | ································$session.GetFiles( | + | ····························$file = ($using:filesEnumerator).Current |
| - | ····································[WinSCP.RemotePath]::EscapeFileMask($remoteFilePath), | + | ····························($using:stats).bytes += $file.Length |
| - | ···································$localFilePath).Check() | + | ($using:stats).count++ |
| - | } | + | $remoteFilePath = $file.FullName |
| } | } | ||
| finally | finally | ||
| { | { | ||
| - | # Disconnect, clean up | + | [System.Threading.Monitor]::Exit($using:filesEnumerator) |
| - | ····························$session.Dispose() | + | |
| } | } | ||
| - | ························ | + | |
| - | Write-Host "Batch $no done" | + | $localFilePath = |
| + | [WinSCP.RemotePath]::TranslateRemotePathToLocal( | ||
| + | $remoteFilePath, $using:remotePath, $using:localPath) | ||
| + | Write-Host "Downloading $remoteFilePath to $localFilePath in $no..." | ||
| + | $localFileDir = (Split-Path -Parent $localFilePath) | ||
| + | New-Item -ItemType directory -Path $localFileDir -Force | Out-Null | ||
| + | $downloadSession.GetFileToDirectory($remoteFilePath, $localFileDir) | | ||
| + | Out-Null | ||
| } | } | ||
| - | catch | + | |
| - | { | + | Write-Host "Download $no done" |
| - | ························Write-Host "Error: $($_.Exception.Message)" | + | ···············} |
| - | exit 1 | + | finally |
| - | ····················} | + | { |
| - | } | Out-Null | + | ···················$downloadSession.Dispose() |
| - | · | + | } |
| - | # Reset for the next batch | + | ···········} | Out-Null |
| - | ···············$no++ | + | |
| - | ················$sum = 0 | + | |
| - | $start = $i + 1 | + | |
| - | ············} | + | |
| } | } | ||
| - | Write-Host "Waiting for batches to complete" | + | Write-Host "Waiting for downloads to complete..." |
| Get-Job | Receive-Job -Wait | Get-Job | Receive-Job -Wait | ||
| + | · | ||
| Write-Host "Done" | Write-Host "Done" | ||
| + | · | ||
| $ended = Get-Date | $ended = Get-Date | ||
| Write-Host "Took $(New-TimeSpan -Start $started -End $ended)" | Write-Host "Took $(New-TimeSpan -Start $started -End $ended)" | ||
| + | Write-Host ("Downloaded $($stats.count) files, " + | ||
| + | "totaling $($stats.bytes.ToString("N0")) bytes") | ||
| } | } | ||
| finally | finally | ||
| Line 266: | Line 227: | ||
| $session.Dispose() | $session.Dispose() | ||
| } | } | ||
| + | · | ||
| exit 0 | exit 0 | ||
| } | } | ||