Search code examples
powershellftpsftpwinscpwinscp-net

Download files from set of folders on FTP/SFTP server with WinSCP .NET assembly in PowerShell and email results


I have this PowerShell script that I'm working on. CSV file is imported to get source and destination paths. The goal is to move files from a SFTP/FTP server into a destination and send an email report.

Task scheduler will run this code every hour. And if there's a new file, as email will be sent out.

It's almost done, but two things are missing:

  • Check if the file already exists and Body email seems empty: Getting the following error: Cannot validate argument on parameter 'Body'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.

  • I would like some assistance on how to check if the file exists and how to get this email if a new file was dropped and copied to the destination list

$SMTPBody = ""

$SMTPMessage  = @{
  "SMTPServer" = ""
  "From" = ""
  "To" = ""
  "Subject" = "New File"
}
try {
  # Load WinSCP .NET assembly
  Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"

  # Setup session options
  $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
    Protocol = [WinSCP.Protocol]::sftp
    HostName = ""
    UserName = ""
    Password = ""
    PortNumber = "22"
    FTPMode = ""
    GiveUpSecurityAndAcceptAnySshHostKey = $true
  }
  $session = New-Object WinSCP.Session
  try
  {
    # Connect
    $session.Open($sessionOptions)
    # Download files
    $transferOptions = New-Object WinSCP.TransferOptions
    $transferOptions.TransferMode = [WinSCP.TransferMode]::Binary

    Import-Csv -Path "D:\FILESOURCE.csv" -ErrorAction Stop |  foreach {

      $synchronizationResult = $session.SynchronizeDirectories(
        [WinSCP.SynchronizationMode]::Local, $_.Destination, $_.Source, $False)
      $synchronizationResult.Check()

      foreach ($download in $synchronizationResult.Downloads ) {
        Write-Host "File $($download.FileName) downloaded" -ForegroundColor Green
        $SMTPBody +=
          "`n Files: $($download.FileName -join ',    ') `n" +
          "Current Location: $($_.Destination)`n"
        Send-MailMessage @SMTPMessage  -Body $SMTPBody
      }

      $transferResult =
        $session.GetFiles($_.Source, $_.Destination, $False, $transferOptions)
      #Find the latest downloaded file
      $latestTransfer =
        $transferResult.Transfers |
        Sort-Object -Property @{ Expression = { (Get-Item $_.Destination).LastWriteTime }
        } -Descending |Select-Object -First 1
    }

    if ($latestTransfer -eq $Null) {
      Write-Host "No files found."
      $SMTPBody += "There are no new files at the moment"
    }
    else
    {
      $lastTimestamp = (Get-Item $latestTransfer.Destination).LastWriteTime
      Write-Host (
        "Downloaded $($transferResult.Transfers.Count) files, " +
        "latest being $($latestTransfer.FileName) with timestamp $lastTimestamp.")
      $SMTPBody += "file : $($latestTransfer)"
    }

    Write-Host "Waiting..."
    Start-Sleep -Seconds 5
  }  
  finally
  {
    Send-MailMessage @SMTPMessage  -Body  $SMTPBody
    # Disconnect, clean up
    $session.Dispose()
  }
}
catch
{
  Write-Host "Error: $($_.Exception.Message)"
}

Solution

  • I believe your code has more problems than you think.

    • Your combination of SynchronizeDirectories and GetFiles is suspicious. You first download only the new files by SynchronizeDirectories and then you download all files by GetFiles. I do not think you want that.
    • On any error the .Check call will throw and you will not collect the error into your report.
    • You keep sending partial reports by Send-MailMessage in the foreach loop

    This is my take on your problem, hoping I've understood correctly what you want to implement:

    $SMTPBody = ""
    
    Import-Csv -Path "FILESOURCE.csv" -ErrorAction Stop |  foreach {
    
        Write-Host "$($_.Source) => $($_.Destination)"
        $SMTPBody += "$($_.Source) => $($_.Destination)`n"
    
        $synchronizationResult =
            $session.SynchronizeDirectories(
                [WinSCP.SynchronizationMode]::Local, $_.Destination, $_.Source, $False)
    
        $downloaded = @()
        $failed = @()
        $latestName = $Null
        $latest = $Null
        foreach ($download in $synchronizationResult.Downloads)
        {
            if ($download.Error -eq $Null)
            {
                Write-Host "File $($download.FileName) downloaded" -ForegroundColor Green
                $downloaded += $download.FileName
                $ts = (Get-Item $download.Destination).LastWriteTime
                if ($ts -gt $latest)
                {
                    $latestName = $download.FileName;
                    $latest = $ts
                }
            }
            else
            {
                Write-Host "File $($download.FileName) download failed" -ForegroundColor Red
                $failed += $download.FileName
            }
        }
    
        if ($downloaded.Count -eq 0)
        {
            $SMTPBody += "No new files were downloaded`n"
        }
        else
        {
            $SMTPBody += 
                "Downloaded $($downloaded.Count) files:`n" +
                ($downloaded -join ", ") + "`n" +
                "latest being $($latestName) with timestamp $latest.`n"
        }
    
        if ($failed.Count -gt 0)
        {
            $SMTPBody += 
                "Failed to download $($failed.Count) files:`n" +
                ($failed -join ", ") + "`n"
        }
    
        $SMTPBody += "`n"
    }
    

    It will give you a report like:

    /source1 => C:\dest1`
    Downloaded 3 files:
    /source1/aaa.txt, /source1/bbb.txt, /source1/ccc.txt
    latest being /source1/ccc.txt with timestamp 01/29/2020 07:49:07.
    
    /source2 => C:\dest2
    Downloaded 1 files:
    /source2/aaa.txt
    latest being /source2/aaa.txt with timestamp 01/29/2020 07:22:37.
    Failed to download 1 files:
    /source2/bbb.txt