From c1b6b9c170a26ce6694d99f5fcd922554d3eff1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Znamen=C3=A1=C4=8Dek?= Date: Mon, 5 Mar 2018 00:27:08 +0100 Subject: [PATCH] Fix #395 : SftpClient Enumerates Rather Than Accumulates Directory Items --- .../SftpClientTest.EnumerateDirectory.cs | 209 ++++++++++++++++++ .../Classes/SftpClientTest.ListDirectory.cs | 48 ++-- src/Renci.SshNet/ISftpClient.cs | 22 ++ src/Renci.SshNet/SftpClient.cs | 86 ++++++- 4 files changed, 347 insertions(+), 18 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Classes/SftpClientTest.EnumerateDirectory.cs diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.EnumerateDirectory.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.EnumerateDirectory.cs new file mode 100644 index 000000000..ef4b3d2ae --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest.EnumerateDirectory.cs @@ -0,0 +1,209 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Tests.Common; +using Renci.SshNet.Tests.Properties; +using System; +using System.Diagnostics; +using System.Linq; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : TestBase + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshConnectionException))] + public void Test_Sftp_EnumerateDirectory_Without_Connecting() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + var files = sftp.EnumerateDirectory("."); + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + } + } + + [TestMethod] + [TestCategory("Sftp")] + [TestCategory("integration")] + [ExpectedException(typeof(SftpPermissionDeniedException))] + public void Test_Sftp_EnumerateDirectory_Permission_Denied() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.EnumerateDirectory("/root"); + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [TestCategory("integration")] + [ExpectedException(typeof(SftpPathNotFoundException))] + public void Test_Sftp_EnumerateDirectory_Not_Exists() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.EnumerateDirectory("/asdfgh"); + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [TestCategory("integration")] + public void Test_Sftp_EnumerateDirectory_Current() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.EnumerateDirectory("."); + + Assert.IsTrue(files.Count() > 0); + + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [TestCategory("integration")] + public void Test_Sftp_EnumerateDirectory_Empty() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.EnumerateDirectory(string.Empty); + + Assert.IsTrue(files.Count() > 0); + + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [TestCategory("integration")] + [Description("Test passing null to EnumerateDirectory.")] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_Sftp_EnumerateDirectory_Null() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.EnumerateDirectory(null); + + Assert.IsTrue(files.Count() > 0); + + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [TestCategory("integration")] + public void Test_Sftp_EnumerateDirectory_HugeDirectory() + { + var stopwatch = Stopwatch.StartNew(); + try + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + sftp.ChangeDirectory("/home/" + Resources.USERNAME); + + var count = 10000; + // Create 10000 directory items + for (int i = 0; i < count; i++) + { + sftp.CreateDirectory(string.Format("test_{0}", i)); + } + Debug.WriteLine(string.Format("Created {0} directories within {1} seconds", count, stopwatch.Elapsed.TotalSeconds)); + + stopwatch.Reset(); stopwatch.Start(); + var files = sftp.EnumerateDirectory("."); + Debug.WriteLine(string.Format("Listed {0} directories within {1} seconds", count, stopwatch.Elapsed.TotalSeconds)); + + // Ensure that directory has at least 10000 items + stopwatch.Reset(); stopwatch.Start(); + var actualCount = files.Count(); + Assert.IsTrue(actualCount >= count); + Debug.WriteLine(string.Format("Used {0} items within {1} seconds", actualCount, stopwatch.Elapsed.TotalSeconds)); + + sftp.Disconnect(); + } + } + finally + { + stopwatch.Reset(); stopwatch.Start(); + RemoveAllFiles(); + stopwatch.Stop(); + Debug.WriteLine(string.Format("Removed all files within {0} seconds", stopwatch.Elapsed.TotalSeconds)); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [TestCategory("integration")] + [ExpectedException(typeof(SshConnectionException))] + public void Test_Sftp_EnumerateDirectory_After_Disconnected() + { + try { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.CreateDirectory("test_at_dsiposed"); + + var files = sftp.EnumerateDirectory(".").Take(1); + + sftp.Disconnect(); + + // Must fail on disconnected session. + var count = files.Count(); + } + } + finally + { + RemoveAllFiles(); + } + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs index 69f90aefe..360c29bab 100644 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs +++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs @@ -140,26 +140,42 @@ public void Test_Sftp_ListDirectory_Null() [TestCategory("integration")] public void Test_Sftp_ListDirectory_HugeDirectory() { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + var stopwatch = Stopwatch.StartNew(); + try { - sftp.Connect(); - - // Create 10000 directory items - for (int i = 0; i < 10000; i++) + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) { - sftp.CreateDirectory(string.Format("test_{0}", i)); - Debug.WriteLine("Created " + i); + sftp.Connect(); + sftp.ChangeDirectory("/home/" + Resources.USERNAME); + + var count = 10000; + // Create 10000 directory items + for (int i = 0; i < count; i++) + { + sftp.CreateDirectory(string.Format("test_{0}", i)); + } + Debug.WriteLine(string.Format("Created {0} directories within {1} seconds", count, stopwatch.Elapsed.TotalSeconds)); + + stopwatch.Reset(); stopwatch.Start(); + var files = sftp.ListDirectory("."); + Debug.WriteLine(string.Format("Listed {0} directories within {1} seconds", count, stopwatch.Elapsed.TotalSeconds)); + + // Ensure that directory has at least 10000 items + stopwatch.Reset(); stopwatch.Start(); + var actualCount = files.Count(); + Assert.IsTrue(actualCount >= count); + Debug.WriteLine(string.Format("Used {0} items within {1} seconds", actualCount, stopwatch.Elapsed.TotalSeconds)); + + sftp.Disconnect(); } - - var files = sftp.ListDirectory("."); - - // Ensure that directory has at least 10000 items - Assert.IsTrue(files.Count() > 10000); - - sftp.Disconnect(); } - - RemoveAllFiles(); + finally + { + stopwatch.Reset(); stopwatch.Start(); + RemoveAllFiles(); + stopwatch.Stop(); + Debug.WriteLine(string.Format("Removed all files within {0} seconds", stopwatch.Elapsed.TotalSeconds)); + } } [TestMethod] diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index b1212cfea..8954793a4 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -668,6 +668,28 @@ public interface ISftpClient /// The method was called after the client was disposed. IEnumerable ListDirectory(string path, Action listCallback = null); + /// + /// Enumerates files and directories in remote directory. + /// + /// + /// This method differs to in the way how the items are returned. + /// It yields the items to the last moment for the enumerator to decide if it needs to continue or stop enumerating the items. + /// It is handy in case of really huge directory contents at remote server - meaning really huge 65 thousand files and more. + /// It also decrease the memory footprint and avoids LOH allocation as happen per call to method. + /// There aren't asynchronous counterpart methods to this because enumerating should happen in your specific asynchronous block. + /// + /// The path. + /// The list callback. + /// + /// An of files and directories ready to be enumerated. + /// + /// is null. + /// Client is not connected. + /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server. + /// A SSH error where is the message from the remote host. + /// The method was called after the client was disposed. + IEnumerable EnumerateDirectory(string path, Action listCallback = null); + /// /// Opens a on the specified path with read/write access. /// diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 3c22290d0..c9a480d19 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -537,6 +537,33 @@ public IEnumerable EndListDirectory(IAsyncResult asyncResult) return ar.EndInvoke(); } + /// + /// Enumerates files and directories in remote directory. + /// + /// + /// This method differs to in the way how the items are returned. + /// It yields the items to the last moment for the enumerator to decide if it needs to continue or stop enumerating the items. + /// It is handy in case of really huge directory contents at remote server - meaning really huge 65 thousand files and more. + /// It also decrease the memory footprint and avoids LOH allocation as happen per call to method. + /// There aren't asynchronous counterpart methods to this because enumerating should happen in your specific asynchronous block. + /// + /// The path. + /// The list callback. + /// + /// An of files and directories ready to be enumerated. + /// + /// is null. + /// Client is not connected. + /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server. + /// A SSH error where is the message from the remote host. + /// The method was called after the client was disposed. + public IEnumerable EnumerateDirectory(string path, Action listCallback = null) + { + CheckDisposed(); + + return InternalEnumerateDirectory(path, listCallback); + } + /// /// Gets reference to remote file or directory. /// @@ -1974,7 +2001,7 @@ private IEnumerable InternalSynchronizeDirectories(string sourcePath, return uploadedFiles; } - #endregion +#endregion /// /// Internals the list directory. @@ -1982,7 +2009,7 @@ private IEnumerable InternalSynchronizeDirectories(string sourcePath, /// The path. /// The list callback. /// - /// A list of files in the specfied directory. + /// A list of files in the specified directory. /// /// is null. /// Client not connected. @@ -2027,6 +2054,61 @@ private IEnumerable InternalListDirectory(string path, Action lis return result; } + /// + /// Internals the list directory. + /// + /// The path. + /// The list callback. + /// + /// A list of files in the specified directory. + /// + /// is null. + /// Client not connected. + private IEnumerable InternalEnumerateDirectory(string path, Action listCallback) + { + if (path == null) + throw new ArgumentNullException("path"); + + if (_sftpSession == null) + throw new SshConnectionException("Client not connected."); + + var fullPath = _sftpSession.GetCanonicalPath(path); + + var handle = _sftpSession.RequestOpenDir(fullPath); + + var basePath = fullPath; + + if (!basePath.EndsWith("/")) + basePath = string.Format("{0}/", fullPath); + + try + { + int count = 0; + var files = _sftpSession.RequestReadDir(handle); + + while (files != null) + { + count += files.Length; + // Call callback to report number of files read + if (listCallback != null) + { + // Execute callback on different thread + ThreadAbstraction.ExecuteThread(() => listCallback(count)); + } + foreach (var file in files) + { + var fullName = string.Format(CultureInfo.InvariantCulture, "{0}{1}", basePath, file.Key); + yield return new SftpFile(_sftpSession, fullName, file.Value); + } + files = _sftpSession.RequestReadDir(handle); + } + } + finally + { + _sftpSession.RequestClose(handle); + } + } + /// /// Internals the download file. ///