This is an old revision of the document!

Recursively move files in directory tree to/from SFTP/FTP server while preserving source directory structure

When moving files to/from the server, WinSCP by defaults moves the subfolders too (removes them from the source directory).

If you want to preserve the source directory structure, you have to implement walking the source explicitly, moving file one by one, and thus preserving the directory structure.

Upload

C#

Use the DirectoryInfo.EnumerateFileSystemInfos method to walk the source local tree.

using System;
using System.Collections.Generic;
using System.IO;
using WinSCP;
 
class Example
{
    public static int Main()
    {
        try
        {
            // Setup session options
            SessionOptions sessionOptions = new SessionOptions
            {
                Protocol = Protocol.Sftp,
                HostName = "example.com",
                UserName = "user",
                Password = "mypassword",
                SshHostKeyFingerprint = "ssh-rsa 2048 xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx"
            };
 
            string localPath = @"C:\data";
            string remotePath = "/home/user/backup";
 
            using (Session session = new Session())
            {
                // Connect
                session.Open(sessionOptions);
 
                // Enumerate files and directories to upload
                IEnumerable<FileSystemInfo> fileInfos = new DirectoryInfo(localPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories);
 
                foreach (FileSystemInfo fileInfo in fileInfos)
                {
                    string remoteFilePath = session.TranslateLocalPathToRemote(fileInfo.FullName, localPath, remotePath);
 
                    if (fileInfo.Attributes.HasFlag(FileAttributes.Directory))
                    {
                        // Create remote subdirectory, if it does not exist yet
                        if (!session.FileExists(remoteFilePath))
                        {
                            session.CreateDirectory(remoteFilePath);
                        }
                    }
                    else
                    {
                        Console.WriteLine(string.Format("Moving file {0}...", fileInfo.FullName));
                        // Upload file and remove original
                        session.PutFiles(fileInfo.FullName, remoteFilePath, true).Check();
                    }
                }
            }
 
            return 0;
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e);
            return 1;
        }
    }
}

PowerShell

# @name         Upload and Delete Files
# @command      powershell.exe -ExecutionPolicy Bypass -File "%EXTENSION_PATH%" -sessionUrl "!S" -remotePath "!/" -sessionLogPath "%SessionLogPath%" -pause !&
# @description  Moves selected local files to a remote directory, but keeps local directory structure
# @flag         ApplyToDirectories
# @version      1
# @homepage     https://winscp.net/eng/docs/library_example_moves_files_keeping_directory_structure
# @require      WinSCP 5.8.4
# @option       SessionLogPath -config sessionlogfile
# @optionspage  https://winscp.net/eng/docs/library_example_moves_files_keeping_directory_structure#options
 
param (
    # Use Generate URL function to obtain a value for -sessionUrl parameter.
    [Parameter(Mandatory = $True)]
    $sessionUrl = "sftp://user:mypassword;fingerprint=ssh-rsa-xx-xx-xx@example.com/",
    [Parameter(Mandatory = $True)]
    $remotePath,
    $sessionLogPath = $Null,
    [Switch]
    $pause = $False,
    [Parameter(Mandatory = $True, ValueFromRemainingArguments = $True, Position = 0)]
    $localPaths
)
 
try
{
    # Load WinSCP .NET assembly
    $assemblyPath = if ($env:WINSCP_PATH) { $env:WINSCP_PATH } else { $PSScriptRoot }
    Add-Type -Path (Join-Path $assemblyPath "WinSCPnet.dll")
 
    # Setup session options from URL
    $sessionOptions = New-Object WinSCP.SessionOptions
    $sessionOptions.ParseUrl($sessionUrl)
 
    $session = New-Object WinSCP.Session
 
    try
    {
        $session.SessionLogPath = $sessionLogPath
 
        # Connect
        $session.Open($sessionOptions)
 
        foreach ($localPath in $localPaths)
        {
            # If the selected item is file, find all contained files and folders recursively
            if (Test-Path $localPath -PathType container)
            {
                $files = @($localPath) + (Get-ChildItem $localPath -Recurse | Select-Object -ExpandProperty FullName)
            }
            else
            {
                $files = $localPath
            }
 
            $parentLocalPath = Split-Path -Parent (Resolve-Path $localPath)
 
            foreach ($localFilePath in $files)
            {
                $remoteFilePath = $session.TranslateLocalPathToRemote($localFilePath, $parentLocalPath, $remotePath)
 
                if (Test-Path $localFilePath -PathType container)
                {
                    # Create remote subdirectory, if it does not exist yet
                    if (!($session.FileExists($remoteFilePath)))
                    {
                        $session.CreateDirectory($remoteFilePath)
                    }
                }
                else
                {
                    Write-Host ("Moving file {0} to {1}..." -f $localFilePath, $remoteFilePath)
                    # Upload file and remove original
                    $session.PutFiles($localFilePath, $remoteFilePath, $True).Check()
                }
            }
        }
 
        & "$env:WINSCP_PATH\WinSCP.exe" "$sessionUrl" /refresh "$remotePath"
    }
    finally
    {
        # Disconnect, clean up
        $session.Dispose()
    }
 
    $result = 0
}
catch [Exception]
{
    Write-Host ("Error: {0}" -f $_.Exception.Message)
    $result = 1
}
 
# Pause if -pause switch was used
if ($pause)
{
    Write-Host "Press any key to exit..."
    [System.Console]::ReadKey() | Out-Null
}
 
exit $result
Options

In the Session log file you can specify a path to a session log file.

Download

For a download, you can use the code from the Recursively download directory tree with custom error handling example.

Just pass a true to the optional remove parameter of the Session.GetFiles.

C#

session.GetFiles(session.EscapeFileMask(fileInfo.FullName), localFilePath, true);

PowerShell

$session.GetFiles($session.EscapeFileMask($fileInfo.FullName), $localFilePath, $True)

Last modified: by martin