diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index 51f108122..f91ba7df1 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -496,6 +496,14 @@ public interface ISftpClient : IBaseClient /// The method was called after the client was disposed. void Delete(string path); + /// + /// Permanently deletes a file on remote machine. + /// + /// The name of the file or directory to be deleted. Wildcard characters are not supported. + /// The to observe. + /// A that represents the asynchronous delete operation. + Task DeleteAsync(string path, CancellationToken cancellationToken = default); + /// /// Deletes remote directory specified by path. /// @@ -508,6 +516,20 @@ public interface ISftpClient : IBaseClient /// The method was called after the client was disposed. void DeleteDirectory(string path); + /// + /// Asynchronously deletes a remote directory. + /// + /// The path of the directory to be deleted. + /// The to observe. + /// A that represents the asynchronous delete operation. + /// is or contains only whitespace characters. + /// Client is not connected. + /// was not found on the remote host. + /// Permission to delete 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 DeleteDirectoryAsync(string path, CancellationToken cancellationToken = default); + /// /// Deletes remote file specified by path. /// diff --git a/src/Renci.SshNet/Sftp/ISftpFile.cs b/src/Renci.SshNet/Sftp/ISftpFile.cs index 02dff7215..9ed99a692 100644 --- a/src/Renci.SshNet/Sftp/ISftpFile.cs +++ b/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 @@ public interface ISftpFile /// void Delete(); + /// + /// Permanently deletes a file on the remote machine. + /// + /// The to observe. + /// A that represents the asynchronous delete operation. + Task DeleteAsync(CancellationToken cancellationToken = default); + /// /// Moves a specified file to a new location on remote machine, providing the option to specify a new file name. /// diff --git a/src/Renci.SshNet/Sftp/ISftpSession.cs b/src/Renci.SshNet/Sftp/ISftpSession.cs index 7baa3dec8..07cf1b8b9 100644 --- a/src/Renci.SshNet/Sftp/ISftpSession.cs +++ b/src/Renci.SshNet/Sftp/ISftpSession.cs @@ -381,6 +381,16 @@ internal interface ISftpSession : ISubsystemSession /// The path. void RequestRmDir(string path); + /// + /// Asynchronously performs an SSH_FXP_RMDIR request. + /// + /// The path. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous SSH_FXP_RMDIR request. + /// + Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default); + /// /// Performs SSH_FXP_SETSTAT request. /// diff --git a/src/Renci.SshNet/Sftp/SftpFile.cs b/src/Renci.SshNet/Sftp/SftpFile.cs index 9c6f72a5d..44694b5be 100644 --- a/src/Renci.SshNet/Sftp/SftpFile.cs +++ b/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 @@ public void Delete() } } + /// + public Task DeleteAsync(CancellationToken cancellationToken = default) + { + return IsDirectory + ? _sftpSession.RequestRmDirAsync(FullName, cancellationToken) + : _sftpSession.RequestRemoveAsync(FullName, cancellationToken); + } + /// /// Moves a specified file to a new location on remote machine, providing the option to specify a new file name. /// diff --git a/src/Renci.SshNet/Sftp/SftpSession.cs b/src/Renci.SshNet/Sftp/SftpSession.cs index 47c66c2ee..a023854bc 100644 --- a/src/Renci.SshNet/Sftp/SftpSession.cs +++ b/src/Renci.SshNet/Sftp/SftpSession.cs @@ -1613,6 +1613,40 @@ public void RequestRmDir(string path) } } + /// + public async Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + +#if NET || NETSTANDARD2_1_OR_GREATER + await using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false)) +#else + using (cancellationToken.Register(s => ((TaskCompletionSource)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); + } + } + /// /// Performs SSH_FXP_REALPATH request. /// diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index a5106b5be..d20a8ad0a 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -424,6 +424,24 @@ public void DeleteDirectory(string path) _sftpSession.RequestRmDir(fullPath); } + /// + 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); + } + /// /// Deletes remote file specified by path. /// @@ -449,18 +467,7 @@ public void DeleteFile(string path) _sftpSession.RequestRemove(fullPath); } - /// - /// Asynchronously deletes remote file specified by path. - /// - /// File to be deleted path. - /// The to observe. - /// A that represents the asynchronous delete operation. - /// is or contains only whitespace characters. - /// Client is not connected. - /// was not found on the remote host. - /// Permission to delete the file 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 DeleteFileAsync(string path, CancellationToken cancellationToken) { CheckDisposed(); @@ -1527,6 +1534,13 @@ public void Delete(string path) file.Delete(); } + /// + public async Task DeleteAsync(string path, CancellationToken cancellationToken = default) + { + var file = await GetAsync(path, cancellationToken).ConfigureAwait(false); + await file.DeleteAsync(cancellationToken).ConfigureAwait(false); + } + /// /// Returns the date and time the specified file or directory was last accessed. /// diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs index 4f5efb08d..a5660fa3c 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs @@ -287,10 +287,10 @@ public async Task Test_Sftp_Change_DirectoryAsync() 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(); } diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs index 1b7068eea..951fdf666 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs @@ -83,8 +83,8 @@ public async Task Create_directory_with_contents_and_list_it_async() 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 @@ public void Test_Sftp_ListDirectory_Permission_Denied() _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();