Skip to content

Commit 1a8839e

Browse files
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 <[email protected]> * 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 <[email protected]>
1 parent ce867d6 commit 1a8839e

File tree

8 files changed

+188
-18
lines changed

8 files changed

+188
-18
lines changed

src/Renci.SshNet/ISftpClient.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,14 @@ public interface ISftpClient : IBaseClient
496496
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
497497
void Delete(string path);
498498

499+
/// <summary>
500+
/// Permanently deletes a file on remote machine.
501+
/// </summary>
502+
/// <param name="path">The name of the file or directory to be deleted. Wildcard characters are not supported.</param>
503+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
504+
/// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
505+
Task DeleteAsync(string path, CancellationToken cancellationToken = default);
506+
499507
/// <summary>
500508
/// Deletes remote directory specified by path.
501509
/// </summary>
@@ -508,6 +516,20 @@ public interface ISftpClient : IBaseClient
508516
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
509517
void DeleteDirectory(string path);
510518

519+
/// <summary>
520+
/// Asynchronously deletes a remote directory.
521+
/// </summary>
522+
/// <param name="path">The path of the directory to be deleted.</param>
523+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
524+
/// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
525+
/// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
526+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
527+
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
528+
/// <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>
529+
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
530+
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
531+
Task DeleteDirectoryAsync(string path, CancellationToken cancellationToken = default);
532+
511533
/// <summary>
512534
/// Deletes remote file specified by path.
513535
/// </summary>

src/Renci.SshNet/Sftp/ISftpFile.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
24

35
namespace Renci.SshNet.Sftp
46
{
@@ -227,6 +229,13 @@ public interface ISftpFile
227229
/// </summary>
228230
void Delete();
229231

232+
/// <summary>
233+
/// Permanently deletes a file on the remote machine.
234+
/// </summary>
235+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
236+
/// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
237+
Task DeleteAsync(CancellationToken cancellationToken = default);
238+
230239
/// <summary>
231240
/// Moves a specified file to a new location on remote machine, providing the option to specify a new file name.
232241
/// </summary>

src/Renci.SshNet/Sftp/ISftpSession.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,16 @@ internal interface ISftpSession : ISubsystemSession
381381
/// <param name="path">The path.</param>
382382
void RequestRmDir(string path);
383383

384+
/// <summary>
385+
/// Asynchronously performs an SSH_FXP_RMDIR request.
386+
/// </summary>
387+
/// <param name="path">The path.</param>
388+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
389+
/// <returns>
390+
/// A task that represents the asynchronous <c>SSH_FXP_RMDIR</c> request.
391+
/// </returns>
392+
Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default);
393+
384394
/// <summary>
385395
/// Performs SSH_FXP_SETSTAT request.
386396
/// </summary>

src/Renci.SshNet/Sftp/SftpFile.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Globalization;
3+
using System.Threading;
4+
using System.Threading.Tasks;
35

46
using Renci.SshNet.Common;
57

@@ -468,6 +470,14 @@ public void Delete()
468470
}
469471
}
470472

473+
/// <inheritdoc/>
474+
public Task DeleteAsync(CancellationToken cancellationToken = default)
475+
{
476+
return IsDirectory
477+
? _sftpSession.RequestRmDirAsync(FullName, cancellationToken)
478+
: _sftpSession.RequestRemoveAsync(FullName, cancellationToken);
479+
}
480+
471481
/// <summary>
472482
/// Moves a specified file to a new location on remote machine, providing the option to specify a new file name.
473483
/// </summary>

src/Renci.SshNet/Sftp/SftpSession.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,40 @@ public void RequestRmDir(string path)
16131613
}
16141614
}
16151615

1616+
/// <inheritdoc />
1617+
public async Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default)
1618+
{
1619+
cancellationToken.ThrowIfCancellationRequested();
1620+
1621+
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
1622+
1623+
#if NET || NETSTANDARD2_1_OR_GREATER
1624+
await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
1625+
#else
1626+
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
1627+
#endif // NET || NETSTANDARD2_1_OR_GREATER
1628+
{
1629+
SendRequest(new SftpRmDirRequest(ProtocolVersion,
1630+
NextRequestId,
1631+
path,
1632+
_encoding,
1633+
response =>
1634+
{
1635+
var exception = GetSftpException(response);
1636+
if (exception is not null)
1637+
{
1638+
tcs.TrySetException(exception);
1639+
}
1640+
else
1641+
{
1642+
tcs.TrySetResult(true);
1643+
}
1644+
}));
1645+
1646+
_ = await tcs.Task.ConfigureAwait(false);
1647+
}
1648+
}
1649+
16161650
/// <summary>
16171651
/// Performs SSH_FXP_REALPATH request.
16181652
/// </summary>

src/Renci.SshNet/SftpClient.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,24 @@ public void DeleteDirectory(string path)
424424
_sftpSession.RequestRmDir(fullPath);
425425
}
426426

427+
/// <inheritdoc />
428+
public async Task DeleteDirectoryAsync(string path, CancellationToken cancellationToken = default)
429+
{
430+
CheckDisposed();
431+
ThrowHelper.ThrowIfNullOrWhiteSpace(path);
432+
433+
if (_sftpSession is null)
434+
{
435+
throw new SshConnectionException("Client not connected.");
436+
}
437+
438+
cancellationToken.ThrowIfCancellationRequested();
439+
440+
var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
441+
442+
await _sftpSession.RequestRmDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
443+
}
444+
427445
/// <summary>
428446
/// Deletes remote file specified by path.
429447
/// </summary>
@@ -449,18 +467,7 @@ public void DeleteFile(string path)
449467
_sftpSession.RequestRemove(fullPath);
450468
}
451469

452-
/// <summary>
453-
/// Asynchronously deletes remote file specified by path.
454-
/// </summary>
455-
/// <param name="path">File to be deleted path.</param>
456-
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
457-
/// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
458-
/// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
459-
/// <exception cref="SshConnectionException">Client is not connected.</exception>
460-
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
461-
/// <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>
462-
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
463-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
470+
/// <inheritdoc />
464471
public async Task DeleteFileAsync(string path, CancellationToken cancellationToken)
465472
{
466473
CheckDisposed();
@@ -1527,6 +1534,13 @@ public void Delete(string path)
15271534
file.Delete();
15281535
}
15291536

1537+
/// <inheritdoc />
1538+
public async Task DeleteAsync(string path, CancellationToken cancellationToken = default)
1539+
{
1540+
var file = await GetAsync(path, cancellationToken).ConfigureAwait(false);
1541+
await file.DeleteAsync(cancellationToken).ConfigureAwait(false);
1542+
}
1543+
15301544
/// <summary>
15311545
/// Returns the date and time the specified file or directory was last accessed.
15321546
/// </summary>

test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,10 @@ public async Task Test_Sftp_Change_DirectoryAsync()
287287

288288
await sftp.ChangeDirectoryAsync("../../", CancellationToken.None).ConfigureAwait(false);
289289

290-
sftp.DeleteDirectory("test1/test1_1");
291-
sftp.DeleteDirectory("test1/test1_2");
292-
sftp.DeleteDirectory("test1/test1_3");
293-
sftp.DeleteDirectory("test1");
290+
await sftp.DeleteDirectoryAsync("test1/test1_1", CancellationToken.None).ConfigureAwait(false);
291+
await sftp.DeleteDirectoryAsync("test1/test1_2", CancellationToken.None).ConfigureAwait(false);
292+
await sftp.DeleteDirectoryAsync("test1/test1_3", CancellationToken.None).ConfigureAwait(false);
293+
await sftp.DeleteDirectoryAsync("test1", CancellationToken.None).ConfigureAwait(false);
294294

295295
sftp.Disconnect();
296296
}

test/Renci.SshNet.IntegrationTests/SftpClientTests.cs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ public async Task Create_directory_with_contents_and_list_it_async()
8383
actualFiles.Add((file.FullName, file.IsRegularFile, file.IsDirectory));
8484
}
8585

86-
_sftpClient.DeleteFile(testFilePath);
87-
_sftpClient.DeleteDirectory(testDirectory);
86+
await _sftpClient.DeleteFileAsync(testFilePath, CancellationToken.None);
87+
await _sftpClient.DeleteDirectoryAsync(testDirectory, CancellationToken.None);
8888

8989
CollectionAssert.AreEquivalent(expectedFiles, actualFiles);
9090
}
@@ -96,6 +96,77 @@ public void Test_Sftp_ListDirectory_Permission_Denied()
9696
_sftpClient.ListDirectory("/root");
9797
}
9898

99+
[TestMethod]
100+
public async Task Create_directory_and_delete_it_async()
101+
{
102+
var testDirectory = "/home/sshnet/sshnet-test";
103+
104+
// Create new directory and check if it exists
105+
await _sftpClient.CreateDirectoryAsync(testDirectory);
106+
Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
107+
108+
await _sftpClient.DeleteDirectoryAsync(testDirectory, CancellationToken.None).ConfigureAwait(false);
109+
110+
Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
111+
}
112+
113+
[TestMethod]
114+
public async Task Create_directory_with_contents_and_delete_contents_then_directory_async()
115+
{
116+
var testDirectory = "/home/sshnet/sshnet-test";
117+
var testFileName = "test-file.txt";
118+
var testFilePath = $"{testDirectory}/{testFileName}";
119+
var testContent = "file content";
120+
121+
// Create new directory and check if it exists
122+
await _sftpClient.CreateDirectoryAsync(testDirectory);
123+
Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
124+
125+
// Upload file and check if it exists
126+
using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
127+
_sftpClient.UploadFile(fileStream, testFilePath);
128+
Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath).ConfigureAwait(false));
129+
130+
await _sftpClient.DeleteFileAsync(testFilePath, CancellationToken.None).ConfigureAwait(false);
131+
132+
Assert.IsFalse(await _sftpClient.ExistsAsync(testFilePath).ConfigureAwait(false));
133+
Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
134+
135+
await _sftpClient.DeleteDirectoryAsync(testDirectory, CancellationToken.None).ConfigureAwait(false);
136+
137+
Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
138+
}
139+
140+
[TestMethod]
141+
public async Task Create_directory_and_delete_it_using_DeleteAsync()
142+
{
143+
var testDirectory = "/home/sshnet/sshnet-test";
144+
145+
// Create new directory and check if it exists
146+
await _sftpClient.CreateDirectoryAsync(testDirectory);
147+
Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
148+
149+
await _sftpClient.DeleteAsync(testDirectory, CancellationToken.None).ConfigureAwait(false);
150+
151+
Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
152+
}
153+
154+
[TestMethod]
155+
public async Task Create_file_and_delete_using_DeleteAsync()
156+
{
157+
var testFileName = "test-file.txt";
158+
var testContent = "file content";
159+
160+
// Upload file and check if it exists
161+
using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
162+
_sftpClient.UploadFile(fileStream, testFileName);
163+
Assert.IsTrue(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false));
164+
165+
await _sftpClient.DeleteAsync(testFileName, CancellationToken.None).ConfigureAwait(false);
166+
167+
Assert.IsFalse(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false));
168+
}
169+
99170
public void Dispose()
100171
{
101172
_sftpClient.Disconnect();

0 commit comments

Comments
 (0)