Automating transfers or synchronization in parallel connections over SFTP/FTP protocol
Advertisement
Download
C#
The example opens by default three parallel connections and uses them to download remote file tree to local folder in parallel.
using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using WinSCP; class Example { public static int Main() { try { // Setup session options var sessionOptions = new SessionOptions { Protocol = Protocol.Sftp, HostName = "example.com", UserName = "user", Password = "mypassword", SshHostKeyFingerprint = "ssh-rsa 2048 xxxxxxxxxxx..." }; const string localPath = @"C:\local\path"; const string remotePath = "/remote/path"; const int batches = 3; var started = DateTime.Now; int count = 0; long bytes = 0; using (var session = new Session()) { Console.WriteLine("Connecting..."); session.Open(sessionOptions); Console.WriteLine("Starting files enumeration..."); var opts = WinSCP.EnumerationOptions.AllDirectories; IEnumerable<RemoteFileInfo> files = session.EnumerateRemoteFiles(remotePath, null, opts); IEnumerator<RemoteFileInfo> filesEnumerator = files.GetEnumerator(); var tasks = new List<Task>(); for (int i = 1; i <= batches; i++) { int no = i; var task = new Task(() => { using (var downloadSession = new Session()) { Console.WriteLine($"Starting download {no}..."); downloadSession.Open(sessionOptions); while (true) { string remoteFilePath; lock (filesEnumerator) { if (!filesEnumerator.MoveNext()) { break; } RemoteFileInfo file = filesEnumerator.Current; bytes += file.Length; count++; remoteFilePath = file.FullName; } string localFilePath = RemotePath.TranslateRemotePathToLocal( remoteFilePath, remotePath, localPath); Console.WriteLine( $"Downloading {remoteFilePath} to {localFilePath} in {no}..."); string localFileDir = Path.GetDirectoryName(localFilePath); Directory.CreateDirectory(localFileDir); downloadSession.GetFileToDirectory(remoteFilePath, localFileDir); } Console.WriteLine($"Download {no} done"); } }); tasks.Add(task); task.Start(); } Console.WriteLine("Waiting for downloads to complete..."); Task.WaitAll(tasks.ToArray()); } Console.WriteLine("Done"); var ended = DateTime.Now; Console.WriteLine($"Took {ended - started}"); Console.WriteLine($"Downloaded {count} files, totaling {bytes:N0} bytes"); return 0; } catch (Exception e) { Console.WriteLine($"Error: {e}"); return 1; } } }
Advertisement
PowerShell
The following code uses 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
.
param ( $sessionUrl = "sftp://user:password;fingerprint=ssh-rsa-xxxxxxxxxxx...@example.com/", $remotePath = "/remote/path/", $localPath = "c:\local\path\", $batches = 3 ) try { $assemblyFilePath = "WinSCPnet.dll" # Load WinSCP .NET assembly Add-Type -Path $assemblyFilePath # Setup session options $sessionOptions = New-Object WinSCP.SessionOptions $sessionOptions.ParseUrl($sessionUrl) $started = Get-Date # Plain variables cannot be modified in job threads $stats = @{ count = 0 bytes = [long]0 } try { # Connect Write-Host "Connecting..." $session = New-Object WinSCP.Session $session.Open($sessionOptions) Write-Host "Starting files enumeration..." $files = $session.EnumerateRemoteFiles( $remotePath, $Null, [WinSCP.EnumerationOptions]::AllDirectories) $filesEnumerator = $files.GetEnumerator() for ($i = 1; $i -le $batches; $i++) { Start-ThreadJob -Name "Batch $i" -ArgumentList $i { param ($no) try { Write-Host "Starting download $no..." $downloadSession = New-Object WinSCP.Session $downloadSession.Open($using:sessionOptions) while ($True) { [System.Threading.Monitor]::Enter($using:filesEnumerator) try { if (!($using:filesEnumerator).MoveNext()) { break } $file = ($using:filesEnumerator).Current ($using:stats).bytes += $file.Length ($using:stats).count++ $remoteFilePath = $file.FullName } finally { [System.Threading.Monitor]::Exit($using:filesEnumerator) } $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 } Write-Host "Download $no done" } finally { $downloadSession.Dispose() } } | Out-Null } Write-Host "Waiting for downloads to complete..." Get-Job | Receive-Job -Wait -ErrorAction Stop Write-Host "Done" $ended = Get-Date Write-Host "Took $(New-TimeSpan -Start $started -End $ended)" Write-Host ("Downloaded $($stats.count) files, " + "totaling $($stats.bytes.ToString("N0")) bytes") } finally { # Disconnect, clean up $session.Dispose() } exit 0 } catch { Write-Host "Error: $($_.Exception.Message)" exit 1 }
Advertisement
Upload
C#
The example opens by default three parallel connections and uses them to upload locale file tree to remote folder in parallel.
using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using WinSCP; class Example { static int Main() { try { // Setup session options SessionOptions sessionOptions = new SessionOptions { Protocol = Protocol.Sftp, HostName = "example.com", UserName = "user", Password = "password", SshHostKeyFingerprint = "ssh-rsa 2048 xxxxxxxxxxx..." }; const string localPath = @"C:\local\path"; const string remotePath = "/remote/path"; const int batches = 3; DateTime started = DateTime.Now; int count = 0; Int64 bytes = 0; Console.WriteLine("Starting files enumeration..."); IEnumerable<string> files = Directory.EnumerateFiles(localPath, "*.*", SearchOption.AllDirectories); IEnumerator<string> filesEnumerator = files.GetEnumerator(); List<Task> tasks = new List<Task>(); HashSet<string> existingRemotePaths = new HashSet<string>(); for (int i = 1; i <= batches; i++) { int no = i; Task task = new Task(() => { using (Session uploadSession = new Session()) { while (true) { string localFilePath; lock (filesEnumerator) { if (!filesEnumerator.MoveNext()) { break; } localFilePath = filesEnumerator.Current; bytes += new FileInfo(localFilePath).Length; count++; } if (!uploadSession.Opened) { Console.WriteLine("Starting upload {0}...", no); uploadSession.Open(sessionOptions); } string remoteFilePath = RemotePath.TranslateLocalPathToRemote( localFilePath, localPath, remotePath); Console.WriteLine( "Uploading {0} to {1} in {2}...", localFilePath, remoteFilePath, no); string path = remoteFilePath.Substring(0, remoteFilePath.LastIndexOf('/')); string current = ""; if (path.Substring(0, 1) == "/") { path = path.Substring(1); } while (!string.IsNullOrEmpty(path)) { int p = path.IndexOf('/'); current += '/'; if (p >= 0) { current += path.Substring(0, p); path = path.Substring(p + 1); } else { current += path; path = ""; } lock (existingRemotePaths) { if (!existingRemotePaths.Contains(current)) // optimization { if (!uploadSession.FileExists(current)) { Console.WriteLine("Creating {0}...", current); uploadSession.CreateDirectory(current); } existingRemotePaths.Add(current); } } } uploadSession.PutFiles( localFilePath, RemotePath.EscapeFileMask(remoteFilePath)). Check(); } if (uploadSession.Opened) { Console.WriteLine("Upload {0} done", no); } else { Console.WriteLine("Upload {0} had nothing to do", no); } } }); tasks.Add(task); task.Start(); } Console.WriteLine("Waiting for uploads to complete..."); Task.WaitAll(tasks.ToArray()); Console.WriteLine("Done"); DateTime ended = DateTime.Now; Console.WriteLine("Took {0}", (ended - started)); Console.WriteLine("Uploaded {0} files, totaling {1:N0} bytes", count, bytes); return 0; } catch (Exception e) { Console.WriteLine("Error: {0}", e); return 1; } } }
Advertisement
Synchronization
PowerShell
Regarding Start-ThreadJob
cmdlet, see the comment in Download section.
param ( $sessionUrl = "sftp://user:password;fingerprint=ssh-rsa-xxxxxxxxxxx...@example.com/", $remotePath = "/remote/path/", $localPath = "c:\local\path\", $removeFiles = $False, $connections = 3 ) try { $assemblyFilePath = "WinSCPnet.dll" # Load WinSCP .NET assembly Add-Type -Path $assemblyFilePath # Setup session options $sessionOptions = New-Object WinSCP.SessionOptions $sessionOptions.ParseUrl($sessionUrl) $started = Get-Date # Plain variables cannot be modified in job threads $stats = @{ count = 0 } try { # Connect Write-Host "Connecting..." $session = New-Object WinSCP.Session $session.Open($sessionOptions) Write-Host "Comparing directories..." $differences = $session.CompareDirectories( [WinSCP.SynchronizationMode]::Both, $localPath, $remotePath, $removeFiles) if ($differences.Count -eq 0) { Write-Host "No changes found." } else { if ($differences.Count -lt $connections) { $connections = $differences.Count; } $differenceEnumerator = $differences.GetEnumerator() for ($i = 1; $i -le $connections; $i++) { Start-ThreadJob -Name "Connection $i" -ArgumentList $i { param ($no) try { Write-Host "Starting connection $no..." $syncSession = New-Object WinSCP.Session $syncSession.Open($using:sessionOptions) while ($True) { [System.Threading.Monitor]::Enter($using:differenceEnumerator) try { if (!($using:differenceEnumerator).MoveNext()) { break } $difference = ($using:differenceEnumerator).Current ($using:stats).count++ } finally { [System.Threading.Monitor]::Exit($using:differenceEnumerator) } Write-Host "$difference in $no..." $difference.Resolve($syncSession) | Out-Null } Write-Host "Connection $no done" } finally { $syncSession.Dispose() } } | Out-Null } Write-Host "Waiting for connections to complete..." Get-Job | Receive-Job -Wait -ErrorAction Stop Write-Host "Done" } $ended = Get-Date Write-Host "Took $(New-TimeSpan -Start $started -End $ended)" Write-Host "Synchronized $($stats.count) differences" } finally { # Disconnect, clean up $session.Dispose() } exit 0 } catch { Write-Host "Error: $($_.Exception.Message)" exit 1 }
Advertisement