powershellmemorystreamwinscpwinscp-net

Creating file on remote directory using MemoryStream, WinSCP and PowerShell


I have read on WinSCP's site that you can create a file on the remote FTP server using the method Session.PutFile and using a MemoryStream to create the object on the remote directory.

The syntax displayed however seems to be missing something
https://winscp.net/eng/docs/library_session_putfile

It mentions to create the MemoryStream and then pass an argument for the remote directory path and the filename.

I am using it within a function I created called SFTPCreatetrigger, as the name states the point is to use this to create an equivalent trigger file with the same name on a separate directory as each file uploads to another.

However I just keep getting the error New-Object:

Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'ComObject'. Specified method is not supported.

Error sys object vs sys string

## ---------------------------------------------------------
## Function to Create file on Remote Directory
## ---------------------------------------------------------
Function SftpCreateTriggerFile
{
    param
    (
    [string]$FtpServer,
    [string]$FtpTriggerDirectory,
    [string]$FtpUsername,
    [string]$FtpPassword,
    [string]$FtpSshHostKeyFingerprint
    )    

    [hashtable]$Return = @{}

    ## Get initial time
    $dt = Get-Date
    $StartTime = Get-Date $dt -f "MMddyy HH:mm:ss"
    $StartTimeUtc = $dt.ToUniversalTime().ToString("MMddyy HH:mm:ss")

    ## Load WinSCP .NET assembly
    [Reflection.Assembly]::LoadFrom("C:\Program Files (x86)\WinSCP\WinSCPnet.dll") | Out-Null

    ## Setup session options
    $sessionOptions = New-Object WinSCP.SessionOptions
    $sessionOptions.Protocol = [WinSCP.Protocol]::Sftp
    $sessionOptions.HostName = $FtpServer
    $sessionOptions.UserName = $FtpUsername
    $sessionOptions.Password = $FtpPassword
    $sessionOptions.PortNumber = 22
    $sessionOptions.SshHostKeyFingerprint = $FtpSshHostKeyFingerprint

    $session = New-Object WinSCP.Session

    try
    {
        # Connect
        $session.Open($sessionOptions)
 
        # Upload files
        $transferOptions = New-Object WinSCP.TransferOptions
        $transferOptions.TransferMode = [WinSCP.TransferMode]::Automatic
        $transferOptions.FilePermissions = $null
        $transferOptions.PreserveTimestamp = $false
        $transferOptions.ResumeSupport.State = [WinSCP.TransferResumeSupportState]::Off

        $transferResult = $session.PutFile((New-Object System.IO.MemoryStream, ($FtpTriggerDirectory + $file.fullname)), $transferOptions)
 
        # Throw on any error
        $transferResult.Check()
 
        if ($transferResult.Transfers.Count -gt 0)
        {
            $Return.IsSuccess = $true
            $Return.StandardOutput = "Success"
        }
        else
        {
            $Return.IsSuccess = $false
            $Return.StandardOutput = "Issue occurred creating trigger file on remote directory $FtpTriggerDirectory. No file created."
        }

        ## Calculate time elapsed
        $ExecTime = [math]::Round(([datetime]::Now - ([datetime]::ParseExact($StartTime,'MMddyy HH:mm:ss',$null))).TotalSeconds)
    }
    catch [Exception]
    {
        $Return.IsSuccess = $false
        $Return.StandardOutput = $_.Exception.Message
        ## Calculate time elapsed
        $ExecTime = [math]::Round(([datetime]::Now - ([datetime]::ParseExact($StartTime,'MMddyy HH:mm:ss',$null))).TotalSeconds)
    }    
    finally
    {
        $Return.StartTime = $StartTime
        $Return.StartTimeUtc = $StartTimeUtc            
        $Return.ExecTime = $ExecTime        
    
        ## Disconnect and cleanup
        $session.Dispose()
    }
    
    Return $Return
}

The above is the function and below is what I am trying to execute

## Set up wildcards to download files
$arrFiles = @(Get-ChildItem $LocalDirectory -Filter "*.txt")


foreach ($file in $arrFiles)
{
    $arrReturnResults = SftpUploadWithWildcards -FtpServer $FtpServer -ftpdirectory $FtpDirectory -FtpUsername $FtpUsername -FtpPassword $FtpPassword -FtpSshHostKeyFingerprint $FtpSshHostKeyFingerprint -LocalDirectory $LocalDirectory -FileNameWildcards $file.Name
    $ProcessName = "Upload($FtpServer):FromDir($LocalDirectory):Files("+$file.Name+")"

    if ($arrReturnResults.IsSuccess)
    {
        $arrResults += New-Object Psobject -property @{Time = $arrReturnResults.StartTime; Task = $ProcessName; Status = "Success"; Details = ""; ExecTime = $arrReturnResults.ExecTime; TimeUtc = $arrReturnResults.StartTimeUtc}
        ## File Archive
        # Move-Item ($LocalDirectory+$file.Name) ($LocalDirectory+"Archive") -Force -ErrorAction Stop
        ## Create 0 byte file with the same name to the trigger folder on remote server
        
        $arrReturnResults = SftpCreateTriggerFile -FtpServer $FtpServer -ftpTriggerdirectory $FtpTriggerDirectory -FtpUsername $FtpUsername -FtpPassword $FtpPassword -FtpSshHostKeyFingerprint $FtpSshHostKeyFingerprint
        $ProcessName = "Upload($FtpServer):FromDir($LocalDirectory):TriggerFiles("+$file.Name+")"

        if ($arrReturnResults.IsSuccess)
        {
            $arrResults += New-Object Psobject -property @{Time = $arrReturnResults.StartTime; Task = $ProcessName; Status = "Success"; Details = ""; ExecTime = $arrReturnResults.ExecTime; TimeUtc = $arrReturnResults.StartTimeUtc}
        }
        else
        {
            $arrResults += New-Object Psobject -property @{Time = $arrReturnResults.StartTime; Task = $ProcessName; Status = "Failed"; Details = $arrReturnResults.StandardOutput; ExecTime = $arrReturnResults.ExecTime; TimeUtc = $arrReturnResults.StartTimeUtc}
        }
    }
    else
    {
        $arrResults += New-Object Psobject -property @{Time = $arrReturnResults.StartTime; Task = $ProcessName; Status = "Failed"; Details = $arrReturnResults.StandardOutput; ExecTime = $arrReturnResults.ExecTime; TimeUtc = $arrReturnResults.StartTimeUtc}
    }
}

All my variables ($ftpserver, $ftpusername, $ftptriggerdirectory etc) are set with correct values. It just seems calling

New-Object System.IO.MemoryStream alone works to create a new MemoryStream object

CanRead      : True
CanSeek      : True
CanWrite     : True
Capacity     : 0
Length       : 0
Position     : 0
CanTimeout   : False
ReadTimeout  : 
WriteTimeout : 

However I cannot assign the name I want to it (which effectively is the last $file to have gone through the foreach loop)

I have read around MemoryStream use and seen that you need to reference a byte array? However I just want it to create an object on the remote directory as stated in examples I found by Martin Prikryl himself.


Solution

  • If your goal was to create an empty file (from a new empty MemoryStream), then the problem is the incorrect syntax to call Session.PutFile with three arguments. It should be:

    $session.PutFile(
        (New-Object System.IO.MemoryStream),      # stream
        ($FtpTriggerDirectory + $file.fullname),  # remoteFilePath
        $transferOptions)                         # options 
    

    (The brackets around $FtpTriggerDirectory + $file.fullname are redundant)


    Also note that Session.PutFile does not return anything. So all your code, that assigns $transferResult and work with it, will fail. The method throws an exception on error. If you want to log the exception/error, do it in your catch [Exception] clause (what you should be doing anyway).