Skip to content

Commit 7cd0487

Browse files
ListDirectoryAsync return IAsyncEnumerable (#1126)
* ListDirectoryAsync return IAsyncEnumerable * Fix documentation * Update README.md * Fix * Add Sftp ListDirectoryAsync test * Revert * Integration tests for ListDirectoryAsync with IAsyncEnumerable
1 parent 1b46264 commit 7cd0487

File tree

8 files changed

+67
-25
lines changed

8 files changed

+67
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Private keys can be encrypted using one of the following cipher methods:
117117
## Framework Support
118118
**SSH.NET** supports the following target frameworks:
119119
* .NETFramework 4.6.2 (and higher)
120-
* .NET Standard 2.0
120+
* .NET Standard 2.0 and 2.1
121121
* .NET 6 (and higher)
122122

123123
## Usage

build/build.proj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
<OutputDirectory>Renci.SshNet\bin\$(Configuration)\netstandard2.0</OutputDirectory>
2626
<Moniker>netstandard2.0</Moniker>
2727
</TargetFrameworkModern>
28+
<TargetFrameworkModern Include=".NETStandard 2.1">
29+
<OutputDirectory>Renci.SshNet\bin\$(Configuration)\netstandard2.1</OutputDirectory>
30+
<Moniker>netstandard2.1</Moniker>
31+
</TargetFrameworkModern>
2832
<TargetFrameworkModern Include=".NET 6.0">
2933
<OutputDirectory>Renci.SshNet\bin\$(Configuration)\net6.0</OutputDirectory>
3034
<Moniker>net6.0</Moniker>

build/nuget/SSH.NET.nuspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
<group targetFramework="net462" />
2020
<group targetFramework="netstandard2.0">
2121
<dependency id="SshNet.Security.Cryptography" version="[1.3.0]" />
22+
</group>
23+
<group targetFramework="netstandard2.1">
24+
<dependency id="SshNet.Security.Cryptography" version="[1.3.0]" />
2225
</group>
2326
<group targetFramework="net6.0">
2427
<dependency id="SshNet.Security.Cryptography" version="[1.3.0]" />

src/Renci.SshNet.IntegrationTests/SftpClientTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,17 @@ public async Task Create_directory_with_contents_and_list_it_async()
6767
Assert.IsTrue(_sftpClient.Exists(testFilePath));
6868

6969
// Check if ListDirectory works
70-
var files = await _sftpClient.ListDirectoryAsync(testDirectory, CancellationToken.None);
71-
72-
_sftpClient.DeleteFile(testFilePath);
73-
_sftpClient.DeleteDirectory(testDirectory);
70+
var files = _sftpClient.ListDirectoryAsync(testDirectory, CancellationToken.None);
7471

7572
var builder = new StringBuilder();
76-
foreach (var file in files)
73+
await foreach (var file in files)
7774
{
7875
builder.AppendLine($"{file.FullName} {file.IsRegularFile} {file.IsDirectory}");
7976
}
8077

78+
_sftpClient.DeleteFile(testFilePath);
79+
_sftpClient.DeleteDirectory(testDirectory);
80+
8181
Assert.AreEqual(@"/home/sshnet/sshnet-test/. False True
8282
/home/sshnet/sshnet-test/.. False True
8383
/home/sshnet/sshnet-test/test-file.txt True False

src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
using System;
55
using System.Diagnostics;
66
using System.Linq;
7+
#if NET6_0_OR_GREATER
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
#endif
711

812
namespace Renci.SshNet.Tests.Classes
913
{
@@ -89,6 +93,30 @@ public void Test_Sftp_ListDirectory_Current()
8993
}
9094
}
9195

96+
#if NET6_0_OR_GREATER
97+
[TestMethod]
98+
[TestCategory("Sftp")]
99+
[TestCategory("integration")]
100+
public async Task Test_Sftp_ListDirectoryAsync_Current()
101+
{
102+
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
103+
{
104+
sftp.Connect();
105+
var cts = new CancellationTokenSource();
106+
cts.CancelAfter(TimeSpan.FromMinutes(1));
107+
var count = 0;
108+
await foreach(var file in sftp.ListDirectoryAsync(".", cts.Token))
109+
{
110+
count++;
111+
Debug.WriteLine(file.FullName);
112+
}
113+
114+
Assert.IsTrue(count > 0);
115+
116+
sftp.Disconnect();
117+
}
118+
}
119+
#endif
92120
[TestMethod]
93121
[TestCategory("Sftp")]
94122
[TestCategory("integration")]
@@ -265,4 +293,4 @@ public void Test_Sftp_Call_EndListDirectory_Twice()
265293
}
266294
}
267295
}
268-
}
296+
}

src/Renci.SshNet/ISftpClient.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public interface ISftpClient : IBaseClient, IDisposable
4040
/// SSH_FXP_DATA protocol fields.
4141
/// </para>
4242
/// <para>
43-
/// The size of the each indivual SSH_FXP_DATA message is limited to the
43+
/// The size of the each individual SSH_FXP_DATA message is limited to the
4444
/// local maximum packet size of the channel, which is set to <c>64 KB</c>
4545
/// for SSH.NET. However, the peer can limit this even further.
4646
/// </para>
@@ -699,21 +699,23 @@ public interface ISftpClient : IBaseClient, IDisposable
699699
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
700700
IEnumerable<ISftpFile> ListDirectory(string path, Action<int> listCallback = null);
701701

702+
#if FEATURE_ASYNC_ENUMERABLE
702703
/// <summary>
703-
/// Asynchronously retrieves list of files in remote directory.
704+
/// Asynchronously enumerates the files in remote directory.
704705
/// </summary>
705706
/// <param name="path">The path.</param>
706707
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
707708
/// <returns>
708-
/// A <see cref="Task{IEnumerable}"/> that represents the asynchronous list operation.
709-
/// The task result contains an enumerable collection of <see cref="SftpFile"/> for the files in the directory specified by <paramref name="path" />.
709+
/// An <see cref="IAsyncEnumerable{T}"/> of <see cref="ISftpFile"/> that represents the asynchronous enumeration operation.
710+
/// The enumeration contains an async stream of <see cref="ISftpFile"/> for the files in the directory specified by <paramref name="path" />.
710711
/// </returns>
711712
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
712713
/// <exception cref="SshConnectionException">Client is not connected.</exception>
713714
/// <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>
714715
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
715716
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
716-
Task<IEnumerable<ISftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken);
717+
IAsyncEnumerable<ISftpFile> ListDirectoryAsync(string path, CancellationToken cancellationToken);
718+
#endif //FEATURE_ASYNC_ENUMERABLE
717719

718720
/// <summary>
719721
/// Opens a <see cref="SftpFileStream"/> on the specified path with read/write access.

src/Renci.SshNet/Renci.SshNet.csproj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@
22
<PropertyGroup>
33
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
44
<AssemblyName>Renci.SshNet</AssemblyName>
5-
<TargetFrameworks>net462;netstandard2.0;net6.0;net7.0</TargetFrameworks>
5+
<TargetFrameworks>net462;netstandard2.0;netstandard2.1;net6.0;net7.0</TargetFrameworks>
66
</PropertyGroup>
77

88
<PropertyGroup Condition=" '$(TargetFramework)' == 'net462' ">
99
<DefineConstants>FEATURE_BINARY_SERIALIZATION;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_DNS_SYNC;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_RIPEMD160</DefineConstants>
1010
</PropertyGroup>
1111

12-
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
12+
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
1313
<PackageReference Include="SshNet.Security.Cryptography" Version="[1.3.0]" />
1414
</ItemGroup>
1515

16-
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
16+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
1717
<DefineConstants>FEATURE_SOCKET_TAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_DNS_SYNC;FEATURE_DNS_APM;FEATURE_DNS_TAP</DefineConstants>
1818
</PropertyGroup>
19+
20+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
21+
<DefineConstants>$(DefineConstants);FEATURE_ASYNC_ENUMERABLE</DefineConstants>
22+
</PropertyGroup>
1923
</Project>

src/Renci.SshNet/SftpClient.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
using Renci.SshNet.Common;
1111
using Renci.SshNet.Sftp;
1212
using System.Threading.Tasks;
13+
#if FEATURE_ASYNC_ENUMERABLE
14+
using System.Runtime.CompilerServices;
15+
#endif
1316

1417
namespace Renci.SshNet
1518
{
@@ -92,7 +95,7 @@ public TimeSpan OperationTimeout
9295
/// SSH_FXP_DATA protocol fields.
9396
/// </para>
9497
/// <para>
95-
/// The size of the each indivual SSH_FXP_DATA message is limited to the
98+
/// The size of the each individual SSH_FXP_DATA message is limited to the
9699
/// local maximum packet size of the channel, which is set to <c>64 KB</c>
97100
/// for SSH.NET. However, the peer can limit this even further.
98101
/// </para>
@@ -584,21 +587,22 @@ public IEnumerable<ISftpFile> ListDirectory(string path, Action<int> listCallbac
584587
return InternalListDirectory(path, listCallback);
585588
}
586589

590+
#if FEATURE_ASYNC_ENUMERABLE
587591
/// <summary>
588-
/// Asynchronously retrieves list of files in remote directory.
592+
/// Asynchronously enumerates the files in remote directory.
589593
/// </summary>
590594
/// <param name="path">The path.</param>
591595
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
592596
/// <returns>
593-
/// A <see cref="Task{IEnumerable}"/> that represents the asynchronous list operation.
594-
/// The task result contains an enumerable collection of <see cref="SftpFile"/> for the files in the directory specified by <paramref name="path" />.
597+
/// An <see cref="IAsyncEnumerable{T}"/> of <see cref="ISftpFile"/> that represents the asynchronous enumeration operation.
598+
/// The enumeration contains an async stream of <see cref="ISftpFile"/> for the files in the directory specified by <paramref name="path" />.
595599
/// </returns>
596600
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
597601
/// <exception cref="SshConnectionException">Client is not connected.</exception>
598602
/// <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>
599603
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
600604
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
601-
public async Task<IEnumerable<ISftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken)
605+
public async IAsyncEnumerable<ISftpFile> ListDirectoryAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken)
602606
{
603607
CheckDisposed();
604608

@@ -616,7 +620,6 @@ public async Task<IEnumerable<ISftpFile>> ListDirectoryAsync(string path, Cancel
616620

617621
var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
618622

619-
var result = new List<SftpFile>();
620623
var handle = await _sftpSession.RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
621624
try
622625
{
@@ -634,18 +637,16 @@ public async Task<IEnumerable<ISftpFile>> ListDirectoryAsync(string path, Cancel
634637

635638
foreach (var file in files)
636639
{
637-
result.Add(new SftpFile(_sftpSession, basePath + file.Key, file.Value));
640+
yield return new SftpFile(_sftpSession, basePath + file.Key, file.Value);
638641
}
639642
}
640-
641643
}
642644
finally
643645
{
644646
await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false);
645647
}
646-
647-
return result;
648648
}
649+
#endif //FEATURE_ASYNC_ENUMERABLE
649650

650651
/// <summary>
651652
/// Begins an asynchronous operation of retrieving list of files in remote directory.

0 commit comments

Comments
 (0)