Kaynağa Gözat

Added support for deleting directories asynchronously (#1503)

* Added support for deleting directories asynchronously

* Clarify that the task represents the asynchronous delete operation

Co-authored-by: Rob Hague <rob.hague00@gmail.com>

* Added DeleteAsync and DeleteDirectoryAsync to ISftpClient

* Inherit docs from interface

* Added additional tests for new async delete functions

* Update list directory test to use async delete methods

* x

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Ryan Esteves 1 yıl önce
ebeveyn
işleme
1a8839ec3e

+ 22 - 0
src/Renci.SshNet/ISftpClient.cs

@@ -496,6 +496,14 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         void Delete(string path);
 
+        /// <summary>
+        /// Permanently deletes a file on remote machine.
+        /// </summary>
+        /// <param name="path">The name of the file or directory to be deleted. Wildcard characters are not supported.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
+        Task DeleteAsync(string path, CancellationToken cancellationToken = default);
+
         /// <summary>
         /// Deletes remote directory specified by path.
         /// </summary>
@@ -508,6 +516,20 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         void DeleteDirectory(string path);
 
+        /// <summary>
+        /// Asynchronously deletes a remote directory.
+        /// </summary>
+        /// <param name="path">The path of the directory to be deleted.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
+        /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to delete 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 DeleteDirectoryAsync(string path, CancellationToken cancellationToken = default);
+
         /// <summary>
         /// Deletes remote file specified by path.
         /// </summary>

+ 9 - 0
src/Renci.SshNet/Sftp/ISftpFile.cs

@@ -1,4 +1,6 @@
 using System;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace Renci.SshNet.Sftp
 {
@@ -227,6 +229,13 @@ namespace Renci.SshNet.Sftp
         /// </summary>
         void Delete();
 
+        /// <summary>
+        /// Permanently deletes a file on the remote machine.
+        /// </summary>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
+        Task DeleteAsync(CancellationToken cancellationToken = default);
+
         /// <summary>
         /// Moves a specified file to a new location on remote machine, providing the option to specify a new file name.
         /// </summary>

+ 10 - 0
src/Renci.SshNet/Sftp/ISftpSession.cs

@@ -381,6 +381,16 @@ namespace Renci.SshNet.Sftp
         /// <param name="path">The path.</param>
         void RequestRmDir(string path);
 
+        /// <summary>
+        /// Asynchronously performs an SSH_FXP_RMDIR request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+        /// <returns>
+        /// A task that represents the asynchronous <c>SSH_FXP_RMDIR</c> request.
+        /// </returns>
+        Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default);
+
         /// <summary>
         /// Performs SSH_FXP_SETSTAT request.
         /// </summary>

+ 10 - 0
src/Renci.SshNet/Sftp/SftpFile.cs

@@ -1,5 +1,7 @@
 using System;
 using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
 
 using Renci.SshNet.Common;
 
@@ -468,6 +470,14 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+        /// <inheritdoc/>
+        public Task DeleteAsync(CancellationToken cancellationToken = default)
+        {
+            return IsDirectory
+                       ? _sftpSession.RequestRmDirAsync(FullName, cancellationToken)
+                       : _sftpSession.RequestRemoveAsync(FullName, cancellationToken);
+        }
+
         /// <summary>
         /// Moves a specified file to a new location on remote machine, providing the option to specify a new file name.
         /// </summary>

+ 34 - 0
src/Renci.SshNet/Sftp/SftpSession.cs

@@ -1613,6 +1613,40 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+        /// <inheritdoc />
+        public async Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+#if NET || NETSTANDARD2_1_OR_GREATER
+            await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
+#else
+            using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
+#endif // NET || NETSTANDARD2_1_OR_GREATER
+            {
+                SendRequest(new SftpRmDirRequest(ProtocolVersion,
+                                                 NextRequestId,
+                                                 path,
+                                                 _encoding,
+                                                 response =>
+                                                     {
+                                                         var exception = GetSftpException(response);
+                                                         if (exception is not null)
+                                                         {
+                                                             tcs.TrySetException(exception);
+                                                         }
+                                                         else
+                                                         {
+                                                             tcs.TrySetResult(true);
+                                                         }
+                                                     }));
+
+                _ = await tcs.Task.ConfigureAwait(false);
+            }
+        }
+
         /// <summary>
         /// Performs SSH_FXP_REALPATH request.
         /// </summary>

+ 26 - 12
src/Renci.SshNet/SftpClient.cs

@@ -424,6 +424,24 @@ namespace Renci.SshNet
             _sftpSession.RequestRmDir(fullPath);
         }
 
+        /// <inheritdoc />
+        public async Task DeleteDirectoryAsync(string path, CancellationToken cancellationToken = default)
+        {
+            CheckDisposed();
+            ThrowHelper.ThrowIfNullOrWhiteSpace(path);
+
+            if (_sftpSession is null)
+            {
+                throw new SshConnectionException("Client not connected.");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+
+            await _sftpSession.RequestRmDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
+        }
+
         /// <summary>
         /// Deletes remote file specified by path.
         /// </summary>
@@ -449,18 +467,7 @@ namespace Renci.SshNet
             _sftpSession.RequestRemove(fullPath);
         }
 
-        /// <summary>
-        /// Asynchronously deletes remote file specified by path.
-        /// </summary>
-        /// <param name="path">File to be deleted path.</param>
-        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
-        /// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
-        /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
-        /// <exception cref="SshConnectionException">Client is not connected.</exception>
-        /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
-        /// <exception cref="SftpPermissionDeniedException">Permission to delete the file 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>
+        /// <inheritdoc />
         public async Task DeleteFileAsync(string path, CancellationToken cancellationToken)
         {
             CheckDisposed();
@@ -1527,6 +1534,13 @@ namespace Renci.SshNet
             file.Delete();
         }
 
+        /// <inheritdoc />
+        public async Task DeleteAsync(string path, CancellationToken cancellationToken = default)
+        {
+            var file = await GetAsync(path, cancellationToken).ConfigureAwait(false);
+            await file.DeleteAsync(cancellationToken).ConfigureAwait(false);
+        }
+
         /// <summary>
         /// Returns the date and time the specified file or directory was last accessed.
         /// </summary>

+ 4 - 4
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs

@@ -287,10 +287,10 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
 
                 await sftp.ChangeDirectoryAsync("../../", CancellationToken.None).ConfigureAwait(false);
 
-                sftp.DeleteDirectory("test1/test1_1");
-                sftp.DeleteDirectory("test1/test1_2");
-                sftp.DeleteDirectory("test1/test1_3");
-                sftp.DeleteDirectory("test1");
+                await sftp.DeleteDirectoryAsync("test1/test1_1", CancellationToken.None).ConfigureAwait(false);
+                await sftp.DeleteDirectoryAsync("test1/test1_2", CancellationToken.None).ConfigureAwait(false);
+                await sftp.DeleteDirectoryAsync("test1/test1_3", CancellationToken.None).ConfigureAwait(false);
+                await sftp.DeleteDirectoryAsync("test1", CancellationToken.None).ConfigureAwait(false);
 
                 sftp.Disconnect();
             }

+ 73 - 2
test/Renci.SshNet.IntegrationTests/SftpClientTests.cs

@@ -83,8 +83,8 @@ namespace Renci.SshNet.IntegrationTests
                 actualFiles.Add((file.FullName, file.IsRegularFile, file.IsDirectory));
             }
 
-            _sftpClient.DeleteFile(testFilePath);
-            _sftpClient.DeleteDirectory(testDirectory);
+            await _sftpClient.DeleteFileAsync(testFilePath, CancellationToken.None);
+            await _sftpClient.DeleteDirectoryAsync(testDirectory, CancellationToken.None);
 
             CollectionAssert.AreEquivalent(expectedFiles, actualFiles);
         }
@@ -96,6 +96,77 @@ namespace Renci.SshNet.IntegrationTests
             _sftpClient.ListDirectory("/root");
         }
 
+        [TestMethod]
+        public async Task Create_directory_and_delete_it_async()
+        {
+            var testDirectory = "/home/sshnet/sshnet-test";
+
+            // Create new directory and check if it exists
+            await _sftpClient.CreateDirectoryAsync(testDirectory);
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+
+            await _sftpClient.DeleteDirectoryAsync(testDirectory, CancellationToken.None).ConfigureAwait(false);
+
+            Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+        }
+
+        [TestMethod]
+        public async Task Create_directory_with_contents_and_delete_contents_then_directory_async()
+        {
+            var testDirectory = "/home/sshnet/sshnet-test";
+            var testFileName = "test-file.txt";
+            var testFilePath = $"{testDirectory}/{testFileName}";
+            var testContent = "file content";
+
+            // Create new directory and check if it exists
+            await _sftpClient.CreateDirectoryAsync(testDirectory);
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+
+            // Upload file and check if it exists
+            using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
+            _sftpClient.UploadFile(fileStream, testFilePath);
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath).ConfigureAwait(false));
+
+            await _sftpClient.DeleteFileAsync(testFilePath, CancellationToken.None).ConfigureAwait(false);
+
+            Assert.IsFalse(await _sftpClient.ExistsAsync(testFilePath).ConfigureAwait(false));
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+
+            await _sftpClient.DeleteDirectoryAsync(testDirectory, CancellationToken.None).ConfigureAwait(false);
+
+            Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+        }
+
+        [TestMethod]
+        public async Task Create_directory_and_delete_it_using_DeleteAsync()
+        {
+            var testDirectory = "/home/sshnet/sshnet-test";
+
+            // Create new directory and check if it exists
+            await _sftpClient.CreateDirectoryAsync(testDirectory);
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+
+            await _sftpClient.DeleteAsync(testDirectory, CancellationToken.None).ConfigureAwait(false);
+
+            Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+        }
+
+        [TestMethod]
+        public async Task Create_file_and_delete_using_DeleteAsync()
+        {
+            var testFileName = "test-file.txt";
+            var testContent = "file content";
+
+            // Upload file and check if it exists
+            using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
+            _sftpClient.UploadFile(fileStream, testFileName);
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false));
+
+            await _sftpClient.DeleteAsync(testFileName, CancellationToken.None).ConfigureAwait(false);
+
+            Assert.IsFalse(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false));
+        }
+
         public void Dispose()
         {
             _sftpClient.Disconnect();