diff --git a/README.md b/README.md
index 5708d0af4..2c0dc015c 100644
--- a/README.md
+++ b/README.md
@@ -117,7 +117,7 @@ Private keys can be encrypted using one of the following cipher methods:
## Framework Support
**SSH.NET** supports the following target frameworks:
* .NETFramework 4.6.2 (and higher)
-* .NET Standard 2.0
+* .NET Standard 2.0 and 2.1
* .NET 6 (and higher)
## Usage
diff --git a/build/build.proj b/build/build.proj
index d2911509c..97b406cf6 100644
--- a/build/build.proj
+++ b/build/build.proj
@@ -25,6 +25,10 @@
Renci.SshNet\bin\$(Configuration)\netstandard2.0
netstandard2.0
+
+ Renci.SshNet\bin\$(Configuration)\netstandard2.1
+ netstandard2.1
+
Renci.SshNet\bin\$(Configuration)\net6.0
net6.0
diff --git a/build/nuget/SSH.NET.nuspec b/build/nuget/SSH.NET.nuspec
index a694592e0..55ce42e28 100644
--- a/build/nuget/SSH.NET.nuspec
+++ b/build/nuget/SSH.NET.nuspec
@@ -19,6 +19,9 @@
+
+
+
diff --git a/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs
index dfb406475..9648587a1 100644
--- a/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs
+++ b/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs
@@ -67,17 +67,17 @@ public async Task Create_directory_with_contents_and_list_it_async()
Assert.IsTrue(_sftpClient.Exists(testFilePath));
// Check if ListDirectory works
- var files = await _sftpClient.ListDirectoryAsync(testDirectory, CancellationToken.None);
-
- _sftpClient.DeleteFile(testFilePath);
- _sftpClient.DeleteDirectory(testDirectory);
+ var files = _sftpClient.ListDirectoryAsync(testDirectory, CancellationToken.None);
var builder = new StringBuilder();
- foreach (var file in files)
+ await foreach (var file in files)
{
builder.AppendLine($"{file.FullName} {file.IsRegularFile} {file.IsDirectory}");
}
+ _sftpClient.DeleteFile(testFilePath);
+ _sftpClient.DeleteDirectory(testDirectory);
+
Assert.AreEqual(@"/home/sshnet/sshnet-test/. False True
/home/sshnet/sshnet-test/.. False True
/home/sshnet/sshnet-test/test-file.txt True False
diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs
index 69f90aefe..6a19ce5a3 100644
--- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs
+++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs
@@ -4,6 +4,10 @@
using System;
using System.Diagnostics;
using System.Linq;
+#if NET6_0_OR_GREATER
+using System.Threading;
+using System.Threading.Tasks;
+#endif
namespace Renci.SshNet.Tests.Classes
{
@@ -89,6 +93,30 @@ public void Test_Sftp_ListDirectory_Current()
}
}
+#if NET6_0_OR_GREATER
+ [TestMethod]
+ [TestCategory("Sftp")]
+ [TestCategory("integration")]
+ public async Task Test_Sftp_ListDirectoryAsync_Current()
+ {
+ using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
+ {
+ sftp.Connect();
+ var cts = new CancellationTokenSource();
+ cts.CancelAfter(TimeSpan.FromMinutes(1));
+ var count = 0;
+ await foreach(var file in sftp.ListDirectoryAsync(".", cts.Token))
+ {
+ count++;
+ Debug.WriteLine(file.FullName);
+ }
+
+ Assert.IsTrue(count > 0);
+
+ sftp.Disconnect();
+ }
+ }
+#endif
[TestMethod]
[TestCategory("Sftp")]
[TestCategory("integration")]
@@ -265,4 +293,4 @@ public void Test_Sftp_Call_EndListDirectory_Twice()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs
index 82117295c..21d05ae7c 100644
--- a/src/Renci.SshNet/ISftpClient.cs
+++ b/src/Renci.SshNet/ISftpClient.cs
@@ -40,7 +40,7 @@ public interface ISftpClient : IBaseClient, IDisposable
/// SSH_FXP_DATA protocol fields.
///
///
- /// The size of the each indivual SSH_FXP_DATA message is limited to the
+ /// The size of the each individual SSH_FXP_DATA message is limited to the
/// local maximum packet size of the channel, which is set to 64 KB
/// for SSH.NET. However, the peer can limit this even further.
///
@@ -699,21 +699,23 @@ public interface ISftpClient : IBaseClient, IDisposable
/// The method was called after the client was disposed.
IEnumerable ListDirectory(string path, Action listCallback = null);
+#if FEATURE_ASYNC_ENUMERABLE
///
- /// Asynchronously retrieves list of files in remote directory.
+ /// Asynchronously enumerates the files in remote directory.
///
/// The path.
/// The to observe.
///
- /// A that represents the asynchronous list operation.
- /// The task result contains an enumerable collection of for the files in the directory specified by .
+ /// An of that represents the asynchronous enumeration operation.
+ /// The enumeration contains an async stream of for the files in the directory specified by .
///
/// 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.
- Task> ListDirectoryAsync(string path, CancellationToken cancellationToken);
+ IAsyncEnumerable ListDirectoryAsync(string path, CancellationToken cancellationToken);
+#endif //FEATURE_ASYNC_ENUMERABLE
///
/// Opens a on the specified path with read/write access.
diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj
index 9fcd62fd3..e55911d55 100644
--- a/src/Renci.SshNet/Renci.SshNet.csproj
+++ b/src/Renci.SshNet/Renci.SshNet.csproj
@@ -2,18 +2,22 @@
false
Renci.SshNet
- net462;netstandard2.0;net6.0;net7.0
+ net462;netstandard2.0;netstandard2.1;net6.0;net7.0
FEATURE_BINARY_SERIALIZATION;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_DNS_SYNC;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_RIPEMD160
-
+
-
+
FEATURE_SOCKET_TAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_DNS_SYNC;FEATURE_DNS_APM;FEATURE_DNS_TAP
+
+
+ $(DefineConstants);FEATURE_ASYNC_ENUMERABLE
+
diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs
index a275413a1..e6f9cb2d6 100644
--- a/src/Renci.SshNet/SftpClient.cs
+++ b/src/Renci.SshNet/SftpClient.cs
@@ -10,6 +10,9 @@
using Renci.SshNet.Common;
using Renci.SshNet.Sftp;
using System.Threading.Tasks;
+#if FEATURE_ASYNC_ENUMERABLE
+using System.Runtime.CompilerServices;
+#endif
namespace Renci.SshNet
{
@@ -92,7 +95,7 @@ public TimeSpan OperationTimeout
/// SSH_FXP_DATA protocol fields.
///
///
- /// The size of the each indivual SSH_FXP_DATA message is limited to the
+ /// The size of the each individual SSH_FXP_DATA message is limited to the
/// local maximum packet size of the channel, which is set to 64 KB
/// for SSH.NET. However, the peer can limit this even further.
///
@@ -584,21 +587,22 @@ public IEnumerable ListDirectory(string path, Action listCallbac
return InternalListDirectory(path, listCallback);
}
+#if FEATURE_ASYNC_ENUMERABLE
///
- /// Asynchronously retrieves list of files in remote directory.
+ /// Asynchronously enumerates the files in remote directory.
///
/// The path.
/// The to observe.
///
- /// A that represents the asynchronous list operation.
- /// The task result contains an enumerable collection of for the files in the directory specified by .
+ /// An of that represents the asynchronous enumeration operation.
+ /// The enumeration contains an async stream of for the files in the directory specified by .
///
/// 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 async Task> ListDirectoryAsync(string path, CancellationToken cancellationToken)
+ public async IAsyncEnumerable ListDirectoryAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken)
{
CheckDisposed();
@@ -616,7 +620,6 @@ public async Task> ListDirectoryAsync(string path, Cancel
var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
- var result = new List();
var handle = await _sftpSession.RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
try
{
@@ -634,18 +637,16 @@ public async Task> ListDirectoryAsync(string path, Cancel
foreach (var file in files)
{
- result.Add(new SftpFile(_sftpSession, basePath + file.Key, file.Value));
+ yield return new SftpFile(_sftpSession, basePath + file.Key, file.Value);
}
}
-
}
finally
{
await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false);
}
-
- return result;
}
+#endif //FEATURE_ASYNC_ENUMERABLE
///
/// Begins an asynchronous operation of retrieving list of files in remote directory.