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.