Differences

This shows you the differences between the selected revisions of the page.

guide_dotnet 2010-07-01 guide_dotnet 2020-12-25 (current)
Line 1: Line 1:
====== SFTP file transfers in .NET ====== ====== SFTP file transfers in .NET ======
 +**//Techniques demonstrated by this article are implemented for you in [[library|WinSCP .NET assembly]]. Using the assembly is recommended approach over implementing these techniques on your own.//**
 +
//This guide describes how to implement SFTP transfer in .NET application using WinSCP.// //This guide describes how to implement SFTP transfer in .NET application using WinSCP.//
-WinSCP is SFTP client with [[scripting]] interface that you can use to automate many operations that it supports, including file transfers, synchronization and other operations. So WinSCP is not a library (e.g. .NET assembly) that you can call directly. Though this guides shows you how to use it seamlessly from the .NET code.+WinSCP is SFTP client with [[scripting]] interface that you can use to automate many operations that it supports, including file transfers, synchronization and other. So WinSCP itself is not a library (e.g. [[library|.NET assembly]]) that you can call directly. Though this guide shows you how to use it seamlessly from the .NET code. 
 + 
 +===== Before Starting =====
Before starting you should: Before starting you should:
Line 10: Line 14:
===== Using WinSCP from .NET Code ===== ===== Using WinSCP from .NET Code =====
-To run ''[[executables|winscp.com]]'' use ''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.process.aspx|System.Diagnostics.Process]]''. This class allows running any executable, possibly redirecting its standard input and output to a stream accessible from .NET code. Code below expects that ''winscp.com'' (''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.filename.aspx|ProcessStartInfo.FileName]]'') can be found in current working directory or in search path. You need to provide full path otherwise. 
- 
-You can use standard input redirection (''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.redirectstandardinput.aspx|ProcessStartInfo.RedirectStandardInput]]'') to feed [[script_commands|scripting commands]], sparing necessity to assemble temporary script file.((Of course unless what you plan to do is actually execution of existing script file.))  
- 
-Redirection of standard output is less useful, as output of WinSCP does not have any predefined form (cannot be parsed). Though it can be useful to capture it, in case you want to show it to a user in your GUI or for diagnostic purposes. 
- 
-To capture results of script, you can use [[logging_xml|XML logging]]. For this you need to instruct WinSCP to store log file using ''/log'' [[commandline|command-line parameter]] (''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.arguments.aspx|ProcessStartInfo.Arguments]]''). 
 +==== [[running]] Running WinSCP Process ====
 +To run ''[[executables|winscp.com]]'' use ''[[dotnet>system.diagnostics.process|System.Diagnostics.Process]]''. This class allows running any executable, possibly redirecting its standard input and output to a stream accessible from .NET code. Code below expects that ''winscp.com'' (''[[dotnet>system.diagnostics.processstartinfo.filename|ProcessStartInfo.FileName]]'') can be found in current working directory or in search path. You need to provide full path otherwise.
<code csharp> <code csharp>
-const string logname = "log.xml"; 
- 
Process winscp = new Process(); Process winscp = new Process();
winscp.StartInfo.FileName = "winscp.com"; winscp.StartInfo.FileName = "winscp.com";
-winscp.StartInfo.Arguments = "/log=" + logname; 
winscp.StartInfo.UseShellExecute = false; winscp.StartInfo.UseShellExecute = false;
-winscp.StartInfo.RedirectStandardInput = true; 
winscp.StartInfo.CreateNoWindow = true; winscp.StartInfo.CreateNoWindow = true;
winscp.Start(); winscp.Start();
</code> </code>
-To feed commands to standard input use ''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardinput.aspx|Process.StandardInput]]'' stream:+==== [[input]] Feeding scripting commands using standard input ==== 
 +You can use standard input redirection (''[[dotnet&gt;system.diagnostics.processstartinfo.redirectstandardinput|ProcessStartInfo.RedirectStandardInput]]'') to feed [[scripting#commands|scripting commands]], sparing necessity to assemble temporary script file.((Of course unless what you plan to do is actually execution of existing script file.))  
 + 
 +To feed commands to standard input use ''[[dotnet&gt;system.diagnostics.process.standardinput|Process.StandardInput]]'' stream:
<code csharp> <code csharp>
 +winscp.StartInfo.RedirectStandardInput = true;
 +...
 +winscp.Start();
 +
winscp.StandardInput.WriteLine("option batch abort"); winscp.StandardInput.WriteLine("option batch abort");
winscp.StandardInput.WriteLine("option confirm off"); winscp.StandardInput.WriteLine("option confirm off");
Line 40: Line 42:
</code> </code>
-Now you need to wait for WinSCP to finish before you can safely start reading the log file+==== [[output]] Capturing outputs of WinSCP process ==== 
- +While you can redirect standard output of WinSCP process, it is actually not very useful, as output of WinSCP does not have any predefined form (cannot be parsed). Though it can be useful to capture it, in case you want to show it to a user in your GUI or for diagnostic purposes.
-<code csharp> +
-winscp.StandardInput.Close(); +
-winscp.WaitForExit(); +
-</code> +
- +
-If you want to collect the output, redirect the standard output before starting WinSCP (''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.redirectstandardoutput.aspx|ProcessStartInfo.RedirectStandardOutput]]'') and read from output stream (''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx|Process.StandardOutput]]''). You need to collect the output before calling ''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.process.waitforexit.aspx|Process.WaitForExit]]''. The output stream has limited capacity. Once it gets filled, WinSCP hangs waiting for free space, never finishing.+
 +If you want to collect the output, redirect the standard output before starting WinSCP (''[[dotnet>system.diagnostics.processstartinfo.redirectstandardoutput|ProcessStartInfo.RedirectStandardOutput]]'') and read from output stream (''[[dotnet>system.diagnostics.process.standardoutput|Process.StandardOutput]]''). You need to continuously collect the output while the script is running. The output stream has limited capacity. Once it gets filled, WinSCP hangs waiting for free space, never finishing. That means you cannot use ''[[dotnet>system.diagnostics.process.waitforexit|Process.WaitForExit]]'' on its own to wait for script to finish. Convenient alternative is ''[[dotnet>system.io.streamreader.readtoend|StreamReader.ReadToEnd]]'':
<code csharp> <code csharp>
winscp.StartInfo.RedirectStandardOutput = true; winscp.StartInfo.RedirectStandardOutput = true;
Line 55: Line 52:
</code> </code>
-Once WinSCP script finishes, you should [[guide_automation#results|check the results]]. First check exit code (''[[http://msdn.microsoft.com/en-us/library/system.diagnostics.process.exitcode.aspx|Process.ExitCode]]'') +==== [[log]] Using log file ===== 
-of the process:+To capture results of script, you can use [[logging_xml|XML logging]]. For this you need to instruct WinSCP to store log file using ''[[commandline#logging|/xmllog]]'' command-line parameter (''[[dotnet>system.diagnostics.processstartinfo.arguments|ProcessStartInfo.Arguments]]'').
<code csharp> <code csharp>
-if (winscp.ExitCode != 0) +const string logname = &quot;log.xml&quot;; 
-{ +... 
- ···/// Error processing +winscp.StartInfo.Arguments = &quot;/xmllog=\"" + logname + &quot;\&quot;"; 
-} +... 
-else +winscp.Start();
-{ +
-····/// Success processing +
-}+
</code> </code>
-To analyse results further, you may parse and interpret the XML log. First learn about [[logging_xml|XML logging]] to understand basic concepts.+Note that before you can safely start reading and parsing the XML log file using tree-based parser (such as ''[[dotnet>system.xml.xmldocument|XmlDocument]]'' or ''[[dotnet>system.xml.xpath.xpathdocument|XPathDocument]]''), you need to [[#exit|wait for WinSCP to finish]]. See example below. 
 + 
 +If you need to read the log file continuously, you need to use stream-based parser (such as ''[[dotnet>system.xml.xmlreader|XmlReader]]''). See [[guide_interpreting_xml_log#continuous|example]].
-The .NET library offers several classes for handling XML documents. The following example uses ''[[http://msdn.microsoft.com/en-us/library/system.xml.xpath.xpathdocument.aspx|System.Xml.XPath.XPathDocument]]'' class.+Following example shows how to use tree-based parsing using ''XPathDocument'':
<code csharp> <code csharp>
Line 89: Line 85:
</code> </code>
-In case of success, you can extract directory listing generated by ''[[script_commands#ls|ls]]'' command inside ''[[logging_xml#ls|ls]]'' element:+In case of success, you can e.g. extract directory listing generated by ''[[scriptcommand_ls|ls]]'' command inside ''[[logging_xml#ls|ls]]'' element:
<code csharp> <code csharp>
XPathNodeIterator files = nav.Select("//w:file", ns); XPathNodeIterator files = nav.Select("//w:file", ns);
-Console.WriteLine(string.Format("There are {0} files and subdirectories:", files.Count));+Console.WriteLine("There are {0} files and subdirectories:", files.Count);
foreach (XPathNavigator file in files) foreach (XPathNavigator file in files)
{ {
Line 100: Line 96:
</code> </code>
-===== Full C# Example =====+//See also guide to [[guide_interpreting_xml_log|*]].// 
 + 
 +==== [[exit]] Waiting for script to complete ==== 
 +Use ''[[dotnet>system.diagnostics.process.waitforexit|Process.WaitForExit]]'' to wait for WinSCP process to finish.  
 + 
 +If you have output stream redirected, you need to first [[#output|read the output stream to the end]]. 
 + 
 +A good practice is to close input stream too, if you have it redirected. 
 + 
 +<code csharp> 
 +// If input stream is redirected only 
 +winscp.StandardInput.Close(); 
 +// If output stream is redirected only 
 +string output = winscp.StandardOutput.ReadToEnd(); 
 +// Wait for process to completely shut down 
 +winscp.WaitForExit(); 
 +</code> 
 + 
 +==== [[exitcode]] Checking exit code ==== 
 +Once WinSCP script finishes, check exit code (''[[dotnet>system.diagnostics.process.exitcode|Process.ExitCode]]'') 
 +of the process: 
 + 
 +<code csharp> 
 +if (winscp.ExitCode != 0) 
 +
 +    // Error processing 
 +
 +else 
 +
 +    // Success processing 
 +
 +</code> 
 + 
 +===== [[csharp_example]] Full C# Example =====
Individual parts of this example are explained in the previous chapter. Individual parts of this example are explained in the previous chapter.
Line 113: Line 142:
const string logname = "log.xml"; const string logname = "log.xml";
-/// Run hidden WinSCP process+// Run hidden WinSCP process
Process winscp = new Process(); Process winscp = new Process();
winscp.StartInfo.FileName = "winscp.com"; winscp.StartInfo.FileName = "winscp.com";
-winscp.StartInfo.Arguments = "/log=" + logname;+winscp.StartInfo.Arguments = "/xmllog=\"" + logname + "\"";
winscp.StartInfo.UseShellExecute = false; winscp.StartInfo.UseShellExecute = false;
winscp.StartInfo.RedirectStandardInput = true; winscp.StartInfo.RedirectStandardInput = true;
Line 123: Line 152:
winscp.Start(); winscp.Start();
-/// Feed in the scripting commands+// Feed in the scripting commands
winscp.StandardInput.WriteLine("option batch abort"); winscp.StandardInput.WriteLine("option batch abort");
winscp.StandardInput.WriteLine("option confirm off"); winscp.StandardInput.WriteLine("option confirm off");
Line 131: Line 160:
winscp.StandardInput.Close(); winscp.StandardInput.Close();
-/// Collect all output (not used in this example)+// Collect all output (not used in this example)
string output = winscp.StandardOutput.ReadToEnd(); string output = winscp.StandardOutput.ReadToEnd();
-/// Wait until WinSCP finishes+// Wait until WinSCP finishes
winscp.WaitForExit(); winscp.WaitForExit();
-/// Parse and interpret the XML log +// Parse and interpret the XML log 
-/// (Note that in case of fatal failure the log file may not exist at all)+// (Note that in case of fatal failure the log file may not exist at all)
XPathDocument log = new XPathDocument(logname); XPathDocument log = new XPathDocument(logname);
XmlNamespaceManager ns = new XmlNamespaceManager(new NameTable()); XmlNamespaceManager ns = new XmlNamespaceManager(new NameTable());
Line 144: Line 173:
XPathNavigator nav = log.CreateNavigator(); XPathNavigator nav = log.CreateNavigator();
-/// Success (0) or error?+// Success (0) or error?
if (winscp.ExitCode != 0) if (winscp.ExitCode != 0)
{ {
    Console.WriteLine("Error occured");     Console.WriteLine("Error occured");
-    /// See if there are any messages associated with the error+    // See if there are any messages associated with the error
    foreach (XPathNavigator message in nav.Select("//w:message", ns))     foreach (XPathNavigator message in nav.Select("//w:message", ns))
    {     {
Line 157: Line 186:
else else
{ {
-    /// It can be worth looking for directory listing even in case of +    // It can be worth looking for directory listing even in case of 
-    /// error as possibly only upload may fail+    // error as possibly only upload may fail
       
    XPathNodeIterator files = nav.Select("//w:file", ns);     XPathNodeIterator files = nav.Select("//w:file", ns);
-    Console.WriteLine(string.Format("There are {0} files and subdirectories:", files.Count));+    Console.WriteLine("There are {0} files and subdirectories:", files.Count);
    foreach (XPathNavigator file in files)     foreach (XPathNavigator file in files)
    {     {
Line 169: Line 198:
</code> </code>
-===== Full VB.NET Example =====+===== [[vbnet_example]] Full VB.NET Example =====
Individual parts of this example are explained in the first chapter. Note that the VB.NET example was not tested. It is based on C# example above though, which was. Feel free to fix it. Individual parts of this example are explained in the first chapter. Note that the VB.NET example was not tested. It is based on C# example above though, which was. Feel free to fix it.
 +
 +There's more robust [[script_vbnet_robust_example|alternative implementation]] available.
<code vbnet> <code vbnet>
Line 185: Line 216:
Dim winscp As Process = New Process() Dim winscp As Process = New Process()
winscp.StartInfo.FileName = "winscp.com" winscp.StartInfo.FileName = "winscp.com"
-winscp.StartInfo.Arguments = "/log=" + logname+winscp.StartInfo.Arguments = "/xmllog=" + logname
winscp.StartInfo.UseShellExecute = False winscp.StartInfo.UseShellExecute = False
winscp.StartInfo.RedirectStandardInput = True winscp.StartInfo.RedirectStandardInput = True
Line 229: Line 260:
       
    Dim files As XPathNodeIterator = nav.Select("//w:file", ns)     Dim files As XPathNodeIterator = nav.Select("//w:file", ns)
-    Console.WriteLine(string.Format("There are {0} files and subdirectories:", files.Count))+    Console.WriteLine("There are {0} files and subdirectories:", files.Count)
    For Each file As XPathNavigator In files     For Each file As XPathNavigator In files
        Console.WriteLine(file.SelectSingleNode("w:filename/@value", ns).Value)         Console.WriteLine(file.SelectSingleNode("w:filename/@value", ns).Value)
Line 237: Line 268:
</code> </code>
-Modified the above example to be a more robust vb.net function implementation: 
-<code> 
-Imports System 
-Imports System.IO 
-Imports System.Diagnostics 
-Imports System.Xml 
-Imports System.Xml.XPath 
-Imports System.Configuration.ConfigurationManager 
- 
-Public Class SFTP 
-    ' SFTP support, built on WinSCP 
-    ' AF Goldberg, July 2010 
-    Public Shared Function PutSFTP(ByRef filename As String, ByRef remotehost As String, ByRef username As String, ByRef password As String, _ 
-                          Optional ByVal outfilename As String = Nothing, Optional ByVal output As String = Nothing, 
-                          Optional ByRef errmsg As String = Nothing) As Boolean 
- 
-        ' Run hidden WinSCP process 
-        Dim winscp As Process = New Process() 
-        Dim logname As String = Path.ChangeExtension(Path.GetTempFileName, "xml") 
-        With winscp.StartInfo 
-            ' SFTPExecutable needs to be defined in app.config to point to winscp.com 
-            Try 
-                .FileName = AppSettings("SFTPExecutable") 
-                If .FileName Is Nothing OrElse .FileName.Length = 0 Then Throw (New Exception("from PutSFTP: SFTPExecutable not set in config file.")) 
-            Catch ex As Exception 
-                errmsg = ex.Message 
-                Return False 
-            End Try 
-            .Arguments = "/log=" + logname 
-            .UseShellExecute = False 
-            .RedirectStandardInput = True 
-            .RedirectStandardOutput = True 
-            .CreateNoWindow = True 
-        End With 
-        Try 
-            winscp.Start() 
-        Catch ex As Exception 
-            errmsg = "from PutSFTP:  Could not run the WinSCP executable " & winscp.StartInfo.FileName & Environment.NewLine & ex.Message 
-            Return False 
-        End Try 
- 
-        ' Feed in the scripting commands 
-        With winscp.StandardInput 
-            .WriteLine("option batch abort") 
-            .WriteLine("option confirm off") 
-            .WriteLine("open sftp://" & username & ":" & password & "@" & remotehost) 
-            If outfilename Is Nothing Then .WriteLine("put " & filename) Else .WriteLine("put " & filename & " """ & outfilename & """") 
-            .Close() 
-        End With 
-        If Not output Is Nothing Then output = winscp.StandardOutput.ReadToEnd() 
- 
-        ' Wait until WinSCP finishes 
-        winscp.WaitForExit() 
- 
-        ' Parse and interpret the XML log 
-        ' (Note that in case of fatal failure the log file may not exist at all) 
-        If Not File.Exists(logname) Then 
-            errmsg = "from PutSFTP:  The WinSCP executable appears to have crashed." 
-            Return False 
-        End If 
- 
-        Dim log As XPathDocument = New XPathDocument(logname) 
-        Dim ns As XmlNamespaceManager = New XmlNamespaceManager(New NameTable()) 
-        ns.AddNamespace("w", "http://winscp.net/schema/session/1.0") 
-        Dim nav As XPathNavigator = log.CreateNavigator() 
- 
-        ' Success (0) or error? 
-        Dim status As Boolean = (winscp.ExitCode = 0) 
-        If Not status Then 
-            errmsg = "from PutSFTP:  There was an error transferring " & filename & "." 
-            ' See if there are any messages associated with the error 
-            For Each message As XPathNavigator In nav.Select("//w:message", ns) 
-                errmsg &= Environment.NewLine & message.Value 
-            Next message 
-        End If 
- 
-        Try 
-            My.Computer.FileSystem.DeleteFile(logname) 
-        Catch ex As Exception 
-            ' at least we tried to clean up 
-        End Try 
- 
-        Return status 
-    End Function 
-End Class 
-</code>