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();