Two-way synchronization with delete with SFTP/FTP server

WinSCP synchronization functionality is state-less. So when there is a file on one side, which is absent on the opposite side, WinSCP is not able to determine, if the file was added on the first side, or removed on the latter. For this reason in the Both direction, WinSCP does not support the Delete files option.

This extension uses a cache file to remember the list of local files after the previous synchronization. It uses the cached list to determine, if the file was added or removed.

You can install this script as a WinSCP extension by using this page URL in the Add Extension command.

To run the script manually use:

powershell.exe -File C:\path\SynchronizeTwoWayDelete.ps1 -sessionUrl "sftp://user:password;fingerprint=ssh-rsa-xxxxxxxxxxx...=@example.com/" -localPath "C:\local" -remotePath "/remote" -listPath "C:\cache\example.txt"
# @name         Two-Way Synchronization with Delete...
# @command      powershell.exe -ExecutionPolicy Bypass -File "%EXTENSION_PATH%" ^
#                   -sessionUrl "!E" -localPath "%LocalPath%" -remotePath "%RemotePath%" ^
#                   -listPath "%ListPath%" -refresh -pause -sessionLogPath "%SessionLogPath%"
# @description  Synchronizes files on local and remote directories including file deletions ^
#                   by remembering a list of previous local files
# @version      2
# @homepage     https://winscp.net/eng/docs/library_example_two_way_synchronize_delete
# @require      WinSCP 5.18.1
# @option       - -run group "Directories"
# @option         LocalPath -run textbox "&Local directory:" "!\"
# @option         RemotePath -run textbox "&Remote directory:" "!/"
# @option       - -run group "Options"
# @option         ListPath -run textbox "Re&member previous local files in:" ^
#                     "%LOCALAPPDATA%\WinSCP\TwoWayDelete\!N.txt"
# @option       - -config group "Logging"
# @option         SessionLogPath -config sessionlogfile
# @optionspage  https://winscp.net/eng/docs/library_example_two_way_synchronize_delete#options
 
param (
    # Use Generate Session URL function to obtain a value for -sessionUrl parameter.
    $sessionUrl = "sftp://user:mypassword;fingerprint=ssh-rsa-xxxxxxxxxxx...=@example.com/",
    [Parameter(Mandatory = $True)]
    $localPath,
    [Parameter(Mandatory = $True)]
    $remotePath,
    [Parameter(Mandatory = $True)]
    $listPath,
    $sessionLogPath = $Null,
    [Switch]
    $pause,
    [Switch]
    $refresh
)
 
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)
 
    $listPath = [Environment]::ExpandEnvironmentVariables($listPath)
    $listDir = (Split-Path -Parent $listPath) 
    New-Item -ItemType directory -Path $listDir -Force | Out-Null 
 
    if (Test-Path $listPath)
    {
        Write-Host "Loading list of previous local files..."
        [string[]]$previousFiles = Get-Content $listPath
    }
    else
    {
        Write-Host "No list of previous local files"
        $previousFiles = @()
    }
 
    $needRefresh = $False
 
    $session = New-Object WinSCP.Session
 
    try
    {
        $session.SessionLogPath = $sessionLogPath
 
        Write-Host "Connecting..."
        $session.Open($sessionOptions)
 
        Write-Host "Comparing files..."
        $differences =
            $session.CompareDirectories(
                [WinSCP.SynchronizationMode]::Both, $localPath, $remotePath, $False)
 
        if ($differences.Count -eq 0)
        {
            Write-Host "No changes found."   
        }
        else
        {
            Write-Host "Synchronizing $($differences.Count) change(s)..."
 
            foreach ($difference in $differences)
            {
                $action = $difference.Action
                if ($action -eq [WinSCP.SynchronizationAction]::UploadNew)
                {
                    if ($previousFiles -contains $difference.Local.FileName)
                    {
                        $difference.Reverse()
                    }
                    else
                    {
                        $needRefresh = $True
                    }
                }
                elseif ($action -eq [WinSCP.SynchronizationAction]::DownloadNew)
                {
                    $localFilePath =
                        [WinSCP.RemotePath]::TranslateRemotePathToLocal(
                            $difference.Remote.FileName, $remotePath, $localPath)
                    if ($previousFiles -contains $localFilePath)
                    {
                        $difference.Reverse()
                        $needRefresh = $True
                    }
                    else
                    {
                        # noop
                    }
                }
                elseif ($action -eq [WinSCP.SynchronizationAction]::DownloadUpdate)
                {
                    # noop
                }
                elseif ($action -eq [WinSCP.SynchronizationAction]::UploadUpdate)
                {
                    $needRefresh = $True
                }
                else
                {
                    throw "Unexpected difference $action"
                }
 
                Write-Host -NoNewline "$difference ..."
                try
                {
                    $difference.Resolve($session) | Out-Null
                    Write-Host -NoNewline " Done."
                }
                finally
                {
                    Write-Host
                }
            }
        }
    }
    finally
    {
        # Disconnect, clean up
        $session.Dispose()
    }
 
    # Refresh the remote directory in WinSCP GUI, if it was changed and -refresh switch was used
    if ($refresh -and $needRefresh)
    {
        Write-Host "Reloading remote directory..."
        & "$env:WINSCP_PATH\WinSCP.exe" "$sessionUrl" /refresh "$remotePath"
    }
 
    Write-Host "Saving current local file list..."
    $localFiles =
        Get-ChildItem -Recurse -Path $localPath |
        Select-Object -ExpandProperty FullName
    Set-Content $listPath $localFiles
 
    Write-Host "Done."
 
    $result = 0
}
catch
{
    Write-Host "Error: $($_.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

Directories

Enter root directories for the synchronization into the two directory boxes. By default the current working directories will be used. The directories can be specified, when executing the extension only.

Options

In the Remember previous local files in box, specify the path to a list text file that will be used to remember the files that existed after the synchronization. The file (and its parent folders) are automatically created, if it does not exist yet. By default, the file is stored in %LOCALAPPDATA%\WinSCP\TwoWayDelete and named after the current session. This options are available, when executing the extension only.

Preferences

These options are available on the Preferences dialog only.

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

In the Keyboard shortcut, you can specify a keyboard shortcut for the extension.

Last modified: by martin