Skip to content

Commit

Permalink
Added support for deleting directories asynchronously (#1503)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
snargledorf and Rob-Hague authored Sep 26, 2024
1 parent ce867d6 commit 1a8839e
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 18 deletions.
22 changes: 22 additions & 0 deletions src/Renci.SshNet/ISftpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,14 @@ public interface ISftpClient : IBaseClient
/// <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>
Expand All @@ -508,6 +516,20 @@ public interface ISftpClient : IBaseClient
/// <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>
Expand Down
9 changes: 9 additions & 0 deletions src/Renci.SshNet/Sftp/ISftpFile.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Renci.SshNet.Sftp
{
Expand Down Expand Up @@ -227,6 +229,13 @@ public interface ISftpFile
/// </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>
Expand Down
10 changes: 10 additions & 0 deletions src/Renci.SshNet/Sftp/ISftpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,16 @@ internal interface ISftpSession : ISubsystemSession
/// <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>
Expand Down
10 changes: 10 additions & 0 deletions src/Renci.SshNet/Sftp/SftpFile.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

using Renci.SshNet.Common;

Expand Down Expand Up @@ -468,6 +470,14 @@ public void Delete()
}
}

/// <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>
Expand Down
34 changes: 34 additions & 0 deletions src/Renci.SshNet/Sftp/SftpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,40 @@ public void RequestRmDir(string path)
}
}

/// <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>
Expand Down
38 changes: 26 additions & 12 deletions src/Renci.SshNet/SftpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,24 @@ public void DeleteDirectory(string path)
_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>
Expand All @@ -449,18 +467,7 @@ public void DeleteFile(string path)
_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();
Expand Down Expand Up @@ -1527,6 +1534,13 @@ public void Delete(string path)
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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
75 changes: 73 additions & 2 deletions test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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();
Expand Down

0 comments on commit 1a8839e

Please sign in to comment.