Ver código fonte

ListDirectoryAsync return IAsyncEnumerable (#1126)

* ListDirectoryAsync return IAsyncEnumerable

* Fix documentation

* Update README.md

* Fix

* Add Sftp ListDirectoryAsync test

* Revert

* Integration tests for ListDirectoryAsync with IAsyncEnumerable
Wojciech Nagórski 2 anos atrás
pai
commit
7cd0487785

+ 1 - 1
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

+ 4 - 0
build/build.proj

@@ -25,6 +25,10 @@
 			<OutputDirectory>Renci.SshNet\bin\$(Configuration)\netstandard2.0</OutputDirectory>
 			<Moniker>netstandard2.0</Moniker>
 		</TargetFrameworkModern>
+		<TargetFrameworkModern Include=".NETStandard 2.1">
+			<OutputDirectory>Renci.SshNet\bin\$(Configuration)\netstandard2.1</OutputDirectory>
+			<Moniker>netstandard2.1</Moniker>
+		</TargetFrameworkModern>
 		<TargetFrameworkModern Include=".NET 6.0">
 			<OutputDirectory>Renci.SshNet\bin\$(Configuration)\net6.0</OutputDirectory>
 			<Moniker>net6.0</Moniker>

+ 3 - 0
build/nuget/SSH.NET.nuspec

@@ -19,6 +19,9 @@
           <group targetFramework="net462" />
           <group targetFramework="netstandard2.0">
             <dependency id="SshNet.Security.Cryptography" version="[1.3.0]" />
+          </group>          
+          <group targetFramework="netstandard2.1">
+            <dependency id="SshNet.Security.Cryptography" version="[1.3.0]" />
           </group>
           <group targetFramework="net6.0">
             <dependency id="SshNet.Security.Cryptography" version="[1.3.0]" />

+ 5 - 5
src/Renci.SshNet.IntegrationTests/SftpClientTests.cs

@@ -67,17 +67,17 @@ namespace IntegrationTests
             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

+ 29 - 1
src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs

@@ -4,6 +4,10 @@ using Renci.SshNet.Tests.Properties;
 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 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
 
+#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 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
     }
-}
+}

+ 7 - 5
src/Renci.SshNet/ISftpClient.cs

@@ -40,7 +40,7 @@ namespace Renci.SshNet
         /// SSH_FXP_DATA protocol fields.
         /// </para>
         /// <para>
-        /// 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 <c>64 KB</c>
         /// for SSH.NET. However, the peer can limit this even further.
         /// </para>
@@ -699,21 +699,23 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         IEnumerable<ISftpFile> ListDirectory(string path, Action<int> listCallback = null);
 
+#if FEATURE_ASYNC_ENUMERABLE
         /// <summary>
-        /// Asynchronously retrieves list of files in remote directory.
+        /// Asynchronously enumerates the files in remote directory.
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
         /// <returns>
-        /// A <see cref="Task{IEnumerable}"/> that represents the asynchronous list operation.
-        /// The task result contains an enumerable collection of <see cref="SftpFile"/> for the files in the directory specified by <paramref name="path" />.
+        /// An <see cref="IAsyncEnumerable{T}"/> of <see cref="ISftpFile"/> that represents the asynchronous enumeration operation.
+        /// The enumeration contains an async stream of <see cref="ISftpFile"/> for the files in the directory specified by <paramref name="path" />.
         /// </returns>
         /// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <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>
         /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
-        Task<IEnumerable<ISftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken);
+        IAsyncEnumerable<ISftpFile> ListDirectoryAsync(string path, CancellationToken cancellationToken);
+#endif //FEATURE_ASYNC_ENUMERABLE
 
         /// <summary>
         /// Opens a <see cref="SftpFileStream"/> on the specified path with read/write access.

+ 7 - 3
src/Renci.SshNet/Renci.SshNet.csproj

@@ -2,18 +2,22 @@
   <PropertyGroup>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <AssemblyName>Renci.SshNet</AssemblyName>
-    <TargetFrameworks>net462;netstandard2.0;net6.0;net7.0</TargetFrameworks>
+	<TargetFrameworks>net462;netstandard2.0;netstandard2.1;net6.0;net7.0</TargetFrameworks>
   </PropertyGroup>
 
   <PropertyGroup Condition=" '$(TargetFramework)' == 'net462' ">
     <DefineConstants>FEATURE_BINARY_SERIALIZATION;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_DNS_SYNC;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_RIPEMD160</DefineConstants>
   </PropertyGroup>
 
-  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
+  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
     <PackageReference Include="SshNet.Security.Cryptography" Version="[1.3.0]" />
   </ItemGroup>
 
-  <PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
+  <PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
     <DefineConstants>FEATURE_SOCKET_TAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_DNS_SYNC;FEATURE_DNS_APM;FEATURE_DNS_TAP</DefineConstants>
   </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' ">
+    <DefineConstants>$(DefineConstants);FEATURE_ASYNC_ENUMERABLE</DefineConstants>
+  </PropertyGroup>
 </Project>

+ 11 - 10
src/Renci.SshNet/SftpClient.cs

@@ -10,6 +10,9 @@ 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
 
 namespace Renci.SshNet
 {
@@ -92,7 +95,7 @@ namespace Renci.SshNet
         /// SSH_FXP_DATA protocol fields.
         /// </para>
         /// <para>
-        /// 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 <c>64 KB</c>
         /// for SSH.NET. However, the peer can limit this even further.
         /// </para>
@@ -584,21 +587,22 @@ namespace Renci.SshNet
             return InternalListDirectory(path, listCallback);
         }
 
+#if FEATURE_ASYNC_ENUMERABLE
         /// <summary>
-        /// Asynchronously retrieves list of files in remote directory.
+        /// Asynchronously enumerates the files in remote directory.
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
         /// <returns>
-        /// A <see cref="Task{IEnumerable}"/> that represents the asynchronous list operation.
-        /// The task result contains an enumerable collection of <see cref="SftpFile"/> for the files in the directory specified by <paramref name="path" />.
+        /// An <see cref="IAsyncEnumerable{T}"/> of <see cref="ISftpFile"/> that represents the asynchronous enumeration operation.
+        /// The enumeration contains an async stream of <see cref="ISftpFile"/> for the files in the directory specified by <paramref name="path" />.
         /// </returns>
         /// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <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>
         /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
-        public async Task<IEnumerable<ISftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken)
+        public async IAsyncEnumerable<ISftpFile> ListDirectoryAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken)
         {
             CheckDisposed();
 
@@ -616,7 +620,6 @@ namespace Renci.SshNet
 
             var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
 
-            var result = new List<SftpFile>();
             var handle = await _sftpSession.RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
             try
             {
@@ -634,18 +637,16 @@ namespace Renci.SshNet
 
                     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
 
         /// <summary>
         /// Begins an asynchronous operation of retrieving list of files in remote directory.