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

Last modified: by martin