Skip to content

Commit 562b95a

Browse files
committed
Fix #395 : SftpClient Enumerates Rather Than Accumulates Directory Items
1 parent 43329ee commit 562b95a

File tree

3 files changed

+315
-2
lines changed

3 files changed

+315
-2
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
using Renci.SshNet.Common;
3+
using Renci.SshNet.Tests.Common;
4+
using Renci.SshNet.Tests.Properties;
5+
using System;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
9+
namespace Renci.SshNet.Tests.Classes
10+
{
11+
/// <summary>
12+
/// Implementation of the SSH File Transfer Protocol (SFTP) over SSH.
13+
/// </summary>
14+
public partial class SftpClientTest : TestBase
15+
{
16+
[TestMethod]
17+
[TestCategory("Sftp")]
18+
[ExpectedException(typeof(SshConnectionException))]
19+
public void Test_Sftp_EnumerateDirectory_Without_Connecting()
20+
{
21+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
22+
{
23+
var files = sftp.EnumerateDirectory(".");
24+
foreach (var file in files)
25+
{
26+
Debug.WriteLine(file.FullName);
27+
}
28+
}
29+
}
30+
31+
[TestMethod]
32+
[TestCategory("Sftp")]
33+
[TestCategory("integration")]
34+
[ExpectedException(typeof(SftpPermissionDeniedException))]
35+
public void Test_Sftp_EnumerateDirectory_Permission_Denied()
36+
{
37+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
38+
{
39+
sftp.Connect();
40+
41+
var files = sftp.EnumerateDirectory("/root");
42+
foreach (var file in files)
43+
{
44+
Debug.WriteLine(file.FullName);
45+
}
46+
47+
sftp.Disconnect();
48+
}
49+
}
50+
51+
[TestMethod]
52+
[TestCategory("Sftp")]
53+
[TestCategory("integration")]
54+
[ExpectedException(typeof(SftpPathNotFoundException))]
55+
public void Test_Sftp_EnumerateDirectory_Not_Exists()
56+
{
57+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
58+
{
59+
sftp.Connect();
60+
61+
var files = sftp.EnumerateDirectory("/asdfgh");
62+
foreach (var file in files)
63+
{
64+
Debug.WriteLine(file.FullName);
65+
}
66+
67+
sftp.Disconnect();
68+
}
69+
}
70+
71+
[TestMethod]
72+
[TestCategory("Sftp")]
73+
[TestCategory("integration")]
74+
public void Test_Sftp_EnumerateDirectory_Current()
75+
{
76+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
77+
{
78+
sftp.Connect();
79+
80+
var files = sftp.EnumerateDirectory(".");
81+
82+
Assert.IsTrue(files.Count() > 0);
83+
84+
foreach (var file in files)
85+
{
86+
Debug.WriteLine(file.FullName);
87+
}
88+
89+
sftp.Disconnect();
90+
}
91+
}
92+
93+
[TestMethod]
94+
[TestCategory("Sftp")]
95+
[TestCategory("integration")]
96+
public void Test_Sftp_EnumerateDirectory_Empty()
97+
{
98+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
99+
{
100+
sftp.Connect();
101+
102+
var files = sftp.EnumerateDirectory(string.Empty);
103+
104+
Assert.IsTrue(files.Count() > 0);
105+
106+
foreach (var file in files)
107+
{
108+
Debug.WriteLine(file.FullName);
109+
}
110+
111+
sftp.Disconnect();
112+
}
113+
}
114+
115+
[TestMethod]
116+
[TestCategory("Sftp")]
117+
[TestCategory("integration")]
118+
[Description("Test passing null to EnumerateDirectory.")]
119+
[ExpectedException(typeof(ArgumentNullException))]
120+
public void Test_Sftp_EnumerateDirectory_Null()
121+
{
122+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
123+
{
124+
sftp.Connect();
125+
126+
var files = sftp.EnumerateDirectory(null);
127+
128+
Assert.IsTrue(files.Count() > 0);
129+
130+
foreach (var file in files)
131+
{
132+
Debug.WriteLine(file.FullName);
133+
}
134+
135+
sftp.Disconnect();
136+
}
137+
}
138+
139+
[TestMethod]
140+
[TestCategory("Sftp")]
141+
[TestCategory("integration")]
142+
public void Test_Sftp_EnumerateDirectory_HugeDirectory()
143+
{
144+
var stopwatch = Stopwatch.StartNew();
145+
try
146+
{
147+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
148+
{
149+
sftp.Connect();
150+
sftp.ChangeDirectory("/home/" + Resources.USERNAME);
151+
152+
var count = 10000;
153+
// Create 10000 directory items
154+
for (int i = 0; i < count; i++)
155+
{
156+
sftp.CreateDirectory(string.Format("test_{0}", i));
157+
}
158+
Debug.WriteLine(string.Format("Created {0} directories within {1} seconds", count, stopwatch.Elapsed.TotalSeconds));
159+
160+
stopwatch.Reset(); stopwatch.Start();
161+
var files = sftp.EnumerateDirectory(".");
162+
Debug.WriteLine(string.Format("Listed {0} directories within {1} seconds", count, stopwatch.Elapsed.TotalSeconds));
163+
164+
// Ensure that directory has at least 10000 items
165+
stopwatch.Reset(); stopwatch.Start();
166+
var actualCount = files.Count();
167+
Assert.IsTrue(actualCount >= count);
168+
Debug.WriteLine(string.Format("Used {0} items within {1} seconds", actualCount, stopwatch.Elapsed.TotalSeconds));
169+
170+
sftp.Disconnect();
171+
}
172+
}
173+
finally
174+
{
175+
stopwatch.Reset(); stopwatch.Start();
176+
RemoveAllFiles();
177+
stopwatch.Stop();
178+
Debug.WriteLine(string.Format("Removed all files within {0} seconds", stopwatch.Elapsed.TotalSeconds));
179+
}
180+
}
181+
182+
[TestMethod]
183+
[TestCategory("Sftp")]
184+
[TestCategory("integration")]
185+
[ExpectedException(typeof(SshConnectionException))]
186+
public void Test_Sftp_EnumerateDirectory_After_Disconnected()
187+
{
188+
try {
189+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
190+
{
191+
sftp.Connect();
192+
193+
sftp.CreateDirectory("test_at_dsiposed");
194+
195+
var files = sftp.EnumerateDirectory(".").Take(1);
196+
197+
sftp.Disconnect();
198+
199+
// Must fail on disconnected session.
200+
var count = files.Count();
201+
}
202+
}
203+
finally
204+
{
205+
RemoveAllFiles();
206+
}
207+
}
208+
}
209+
}

src/Renci.SshNet/ISftpClient.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,28 @@ public interface ISftpClient : IBaseClient, IDisposable
717717
IAsyncEnumerable<ISftpFile> ListDirectoryAsync(string path, CancellationToken cancellationToken);
718718
#endif //FEATURE_ASYNC_ENUMERABLE
719719

720+
/// <summary>
721+
/// Enumerates files and directories in remote directory.
722+
/// </summary>
723+
/// <remarks>
724+
/// This method differs to <see cref="ListDirectory(string, Action{int})"/> in the way how the items are returned.
725+
/// It yields the items to the last moment for the enumerator to decide if it needs to continue or stop enumerating the items.
726+
/// It is handy in case of really huge directory contents at remote server - meaning really huge 65 thousand files and more.
727+
/// It also decrease the memory footprint and avoids LOH allocation as happen per call to <see cref="ListDirectory(string, Action{int})"/> method.
728+
/// There aren't asynchronous counterpart methods to this because enumerating should happen in your specific asynchronous block.
729+
/// </remarks>
730+
/// <param name="path">The path.</param>
731+
/// <param name="listCallback">The list callback.</param>
732+
/// <returns>
733+
/// An <see cref="System.Collections.Generic.IEnumerable{SftpFile}"/> of files and directories ready to be enumerated.
734+
/// </returns>
735+
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
736+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
737+
/// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
738+
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
739+
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
740+
IEnumerable<SftpFile> EnumerateDirectory(string path, Action<int> listCallback = null);
741+
720742
/// <summary>
721743
/// Opens a <see cref="SftpFileStream"/> on the specified path with read/write access.
722744
/// </summary>

src/Renci.SshNet/SftpClient.cs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,33 @@ public IEnumerable<ISftpFile> EndListDirectory(IAsyncResult asyncResult)
706706
return ar.EndInvoke();
707707
}
708708

709+
/// <summary>
710+
/// Enumerates files and directories in remote directory.
711+
/// </summary>
712+
/// <remarks>
713+
/// This method differs to <see cref="ListDirectory(string, Action{int})"/> in the way how the items are returned.
714+
/// It yields the items to the last moment for the enumerator to decide if it needs to continue or stop enumerating the items.
715+
/// It is handy in case of really huge directory contents at remote server - meaning really huge 65 thousand files and more.
716+
/// It also decrease the memory footprint and avoids LOH allocation as happen per call to <see cref="ListDirectory(string, Action{int})"/> method.
717+
/// There aren't asynchronous counterpart methods to this because enumerating should happen in your specific asynchronous block.
718+
/// </remarks>
719+
/// <param name="path">The path.</param>
720+
/// <param name="listCallback">The list callback.</param>
721+
/// <returns>
722+
/// An <see cref="System.Collections.Generic.IEnumerable{SftpFile}"/> of files and directories ready to be enumerated.
723+
/// </returns>
724+
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
725+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
726+
/// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
727+
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
728+
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
729+
public IEnumerable<SftpFile> EnumerateDirectory(string path, Action<int> listCallback = null)
730+
{
731+
CheckDisposed();
732+
733+
return InternalEnumerateDirectory(path, listCallback);
734+
}
735+
709736
/// <summary>
710737
/// Gets reference to remote file or directory.
711738
/// </summary>
@@ -2261,15 +2288,15 @@ private IEnumerable<FileInfo> InternalSynchronizeDirectories(string sourcePath,
22612288
return uploadedFiles;
22622289
}
22632290

2264-
#endregion
2291+
#endregion
22652292

22662293
/// <summary>
22672294
/// Internals the list directory.
22682295
/// </summary>
22692296
/// <param name="path">The path.</param>
22702297
/// <param name="listCallback">The list callback.</param>
22712298
/// <returns>
2272-
/// A list of files in the specfied directory.
2299+
/// A list of files in the specified directory.
22732300
/// </returns>
22742301
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
22752302
/// <exception cref="SshConnectionException">Client not connected.</exception>
@@ -2324,6 +2351,61 @@ private IEnumerable<ISftpFile> InternalListDirectory(string path, Action<int> li
23242351
return result;
23252352
}
23262353

2354+
/// <summary>
2355+
/// Internals the list directory.
2356+
/// </summary>
2357+
/// <param name="path">The path.</param>
2358+
/// <param name="listCallback">The list callback.</param>
2359+
/// <returns>
2360+
/// A list of files in the specified directory.
2361+
/// </returns>
2362+
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
2363+
/// <exception cref="SshConnectionException">Client not connected.</exception>
2364+
private IEnumerable<SftpFile> InternalEnumerateDirectory(string path, Action<int> listCallback)
2365+
{
2366+
if (path == null)
2367+
throw new ArgumentNullException("path");
2368+
2369+
if (_sftpSession == null)
2370+
throw new SshConnectionException("Client not connected.");
2371+
2372+
var fullPath = _sftpSession.GetCanonicalPath(path);
2373+
2374+
var handle = _sftpSession.RequestOpenDir(fullPath);
2375+
2376+
var basePath = fullPath;
2377+
2378+
if (!basePath.EndsWith("/"))
2379+
basePath = string.Format("{0}/", fullPath);
2380+
2381+
try
2382+
{
2383+
int count = 0;
2384+
var files = _sftpSession.RequestReadDir(handle);
2385+
2386+
while (files != null)
2387+
{
2388+
count += files.Length;
2389+
// Call callback to report number of files read
2390+
if (listCallback != null)
2391+
{
2392+
// Execute callback on different thread
2393+
ThreadAbstraction.ExecuteThread(() => listCallback(count));
2394+
}
2395+
foreach (var file in files)
2396+
{
2397+
var fullName = string.Format(CultureInfo.InvariantCulture, "{0}{1}", basePath, file.Key);
2398+
yield return new SftpFile(_sftpSession, fullName, file.Value);
2399+
}
2400+
files = _sftpSession.RequestReadDir(handle);
2401+
}
2402+
}
2403+
finally
2404+
{
2405+
_sftpSession.RequestClose(handle);
2406+
}
2407+
}
2408+
23272409
/// <summary>
23282410
/// Internals the download file.
23292411
/// </summary>

0 commit comments

Comments
 (0)