diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.EnumerateDirectory.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.EnumerateDirectory.cs new file mode 100644 index 000000000..15de7b038 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.EnumerateDirectory.cs @@ -0,0 +1,209 @@ +using System.Diagnostics; + +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshConnectionException))] + public void Test_Sftp_EnumerateDirectory_Without_Connecting() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.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(SshServerHostName, SshServerPort, User.UserName, User.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(SshServerHostName, SshServerPort, User.UserName, User.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(SshServerHostName, SshServerPort, User.UserName, User.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(SshServerHostName, SshServerPort, User.UserName, User.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(SshServerHostName, SshServerPort, User.UserName, User.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(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + sftp.ChangeDirectory("/home/" + User.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(SshServerHostName, SshServerPort, User.UserName, User.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(); + } + } + } +} diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index 29f958d9f..d41aa6aad 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -718,6 +718,28 @@ public interface ISftpClient : IBaseClient, IDisposable IAsyncEnumerable ListDirectoryAsync(string path, CancellationToken cancellationToken); #endif //FEATURE_ASYNC_ENUMERABLE + /// + /// 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 877fed092..8003ed29d 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -1,15 +1,16 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Globalization; +using System.IO; using System.Net; using System.Text; using System.Threading; +using System.Threading.Tasks; + using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Sftp; -using System.Threading.Tasks; #if FEATURE_ASYNC_ENUMERABLE using System.Runtime.CompilerServices; #endif @@ -706,6 +707,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. /// @@ -1613,7 +1641,7 @@ public Task OpenAsync(string path, FileMode mode, FileAccess acc cancellationToken.ThrowIfCancellationRequested(); - return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken); + return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int) _bufferSize, cancellationToken); } /// @@ -2273,7 +2301,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. @@ -2328,6 +2356,67 @@ private IEnumerable InternalListDirectory(string path, Action li 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 + { + var 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. ///