Skip to content

Commit 68c6793

Browse files
committed
Added ExistsAsync to SftpClient
1 parent 3b55ba3 commit 68c6793

File tree

5 files changed

+235
-2
lines changed

5 files changed

+235
-2
lines changed

src/Renci.SshNet/Sftp/ISftpSession.cs

+11
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ internal interface ISftpSession : ISubsystemSession
116116
/// </returns>
117117
SftpFileAttributes RequestLStat(string path);
118118

119+
/// <summary>
120+
/// Asynchronously performs SSH_FXP_LSTAT request.
121+
/// </summary>
122+
/// <param name="path">The path.</param>
123+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
124+
/// <returns>
125+
/// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
126+
/// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
127+
/// </returns>
128+
Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken);
129+
119130
/// <summary>
120131
/// Performs SSH_FXP_LSTAT request.
121132
/// </summary>

src/Renci.SshNet/Sftp/SftpSession.cs

+32
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,38 @@ public SftpFileAttributes RequestLStat(string path)
10311031
return attributes;
10321032
}
10331033

1034+
/// <summary>
1035+
/// Asynchronously performs SSH_FXP_LSTAT request.
1036+
/// </summary>
1037+
/// <param name="path">The path.</param>
1038+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
1039+
/// <returns>
1040+
/// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
1041+
/// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
1042+
/// </returns>
1043+
public async Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken)
1044+
{
1045+
cancellationToken.ThrowIfCancellationRequested();
1046+
1047+
var tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);
1048+
1049+
#if NET || NETSTANDARD2_1_OR_GREATER
1050+
await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
1051+
#else
1052+
using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
1053+
#endif // NET || NETSTANDARD2_1_OR_GREATER
1054+
{
1055+
SendRequest(new SftpLStatRequest(ProtocolVersion,
1056+
NextRequestId,
1057+
path,
1058+
_encoding,
1059+
response => tcs.TrySetResult(response.Attributes),
1060+
response => tcs.TrySetException(GetSftpException(response))));
1061+
1062+
return await tcs.Task.ConfigureAwait(false);
1063+
}
1064+
}
1065+
10341066
/// <summary>
10351067
/// Performs SSH_FXP_LSTAT request.
10361068
/// </summary>

src/Renci.SshNet/SftpClient.cs

+58
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,64 @@ public bool Exists(string path)
743743
}
744744
}
745745

746+
/// <summary>
747+
/// Checks whether file or directory exists.
748+
/// </summary>
749+
/// <param name="path">The path.</param>
750+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
751+
/// <returns>
752+
/// A <see cref="Task{T}"/> that represents the exists operation.
753+
/// The task result contains <see langword="true"/> if directory or file exists; otherwise <see langword="false"/>.
754+
/// </returns>
755+
/// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
756+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
757+
/// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
758+
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
759+
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
760+
public async Task<bool> ExistsAsync(string path, CancellationToken cancellationToken = default)
761+
{
762+
CheckDisposed();
763+
ThrowHelper.ThrowIfNullOrWhiteSpace(path);
764+
765+
if (_sftpSession is null)
766+
{
767+
throw new SshConnectionException("Client not connected.");
768+
}
769+
770+
cancellationToken.ThrowIfCancellationRequested();
771+
772+
var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
773+
774+
/*
775+
* Using SSH_FXP_REALPATH is not an alternative as the SFTP specification has not always
776+
* been clear on how the server should respond when the specified path is not present on
777+
* the server:
778+
*
779+
* SSH 1 to 4:
780+
* No mention of how the server should respond if the path is not present on the server.
781+
*
782+
* SSH 5:
783+
* The server SHOULD fail the request if the path is not present on the server.
784+
*
785+
* SSH 6:
786+
* Draft 06: The server SHOULD fail the request if the path is not present on the server.
787+
* Draft 07 to 13: The server MUST NOT fail the request if the path does not exist.
788+
*
789+
* Note that SSH 6 (draft 06 and forward) allows for more control options, but we
790+
* currently only support up to v3.
791+
*/
792+
793+
try
794+
{
795+
_ = await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
796+
return true;
797+
}
798+
catch (SftpPathNotFoundException)
799+
{
800+
return false;
801+
}
802+
}
803+
746804
/// <summary>
747805
/// Downloads remote file specified by the path into the stream.
748806
/// </summary>

test/Renci.SshNet.IntegrationTests/SftpClientTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ public async Task Create_directory_with_contents_and_list_it_async()
6161

6262
// Create new directory and check if it exists
6363
_sftpClient.CreateDirectory(testDirectory);
64-
Assert.IsTrue(_sftpClient.Exists(testDirectory));
64+
Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory));
6565

6666
// Upload file and check if it exists
6767
using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
6868
_sftpClient.UploadFile(fileStream, testFilePath);
69-
Assert.IsTrue(_sftpClient.Exists(testFilePath));
69+
Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath));
7070

7171
// Check if ListDirectory works
7272
var expectedFiles = new List<(string FullName, bool IsRegularFile, bool IsDirectory)>()

test/Renci.SshNet.IntegrationTests/SftpTests.cs

+132
Original file line numberDiff line numberDiff line change
@@ -3770,6 +3770,138 @@ public void Sftp_Exists()
37703770
#endregion Teardown
37713771
}
37723772

3773+
[TestMethod]
3774+
public async Task Sftp_ExistsAsync()
3775+
{
3776+
const string remoteHome = "/home/sshnet";
3777+
3778+
#region Setup
3779+
3780+
using (var client = new SshClient(_connectionInfoFactory.Create()))
3781+
{
3782+
client.Connect();
3783+
3784+
#region Clean-up
3785+
3786+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}"))
3787+
{
3788+
await command.ExecuteAsync();
3789+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3790+
}
3791+
3792+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}"))
3793+
{
3794+
await command.ExecuteAsync();
3795+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3796+
}
3797+
3798+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}")
3799+
)
3800+
{
3801+
await command.ExecuteAsync();
3802+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3803+
}
3804+
3805+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}"))
3806+
{
3807+
await command.ExecuteAsync();
3808+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3809+
}
3810+
3811+
using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}"))
3812+
{
3813+
await command.ExecuteAsync();
3814+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3815+
}
3816+
3817+
#endregion Clean-up
3818+
3819+
#region Setup
3820+
3821+
using (var command = client.CreateCommand($"touch {remoteHome + "/file.exists"}"))
3822+
{
3823+
await command.ExecuteAsync();
3824+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3825+
}
3826+
3827+
using (var command = client.CreateCommand($"mkdir {remoteHome + "/directory.exists"}"))
3828+
{
3829+
await command.ExecuteAsync();
3830+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3831+
}
3832+
3833+
using (var command = client.CreateCommand($"ln -s {remoteHome + "/file.exists"} {remoteHome + "/symlink.to.file.exists"}"))
3834+
{
3835+
await command.ExecuteAsync();
3836+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3837+
}
3838+
3839+
using (var command = client.CreateCommand($"ln -s {remoteHome + "/directory.exists"} {remoteHome + "/symlink.to.directory.exists"}"))
3840+
{
3841+
await command.ExecuteAsync();
3842+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3843+
}
3844+
3845+
#endregion Setup
3846+
}
3847+
3848+
#endregion Setup
3849+
3850+
#region Assert
3851+
3852+
using (var client = new SftpClient(_connectionInfoFactory.Create()))
3853+
{
3854+
await client.ConnectAsync(default).ConfigureAwait(false);
3855+
3856+
Assert.IsFalse(await client.ExistsAsync(remoteHome + "/DoesNotExist"));
3857+
Assert.IsTrue(await client.ExistsAsync(remoteHome + "/file.exists"));
3858+
Assert.IsTrue(await client.ExistsAsync(remoteHome + "/symlink.to.file.exists"));
3859+
Assert.IsTrue(await client.ExistsAsync(remoteHome + "/directory.exists"));
3860+
Assert.IsTrue(await client.ExistsAsync(remoteHome + "/symlink.to.directory.exists"));
3861+
}
3862+
3863+
#endregion Assert
3864+
3865+
#region Teardown
3866+
3867+
using (var client = new SshClient(_connectionInfoFactory.Create()))
3868+
{
3869+
client.Connect();
3870+
3871+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}"))
3872+
{
3873+
await command.ExecuteAsync();
3874+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3875+
}
3876+
3877+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}"))
3878+
{
3879+
await command.ExecuteAsync();
3880+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3881+
}
3882+
3883+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}"))
3884+
{
3885+
await command.ExecuteAsync();
3886+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3887+
}
3888+
3889+
using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}"))
3890+
{
3891+
await command.ExecuteAsync();
3892+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3893+
}
3894+
3895+
using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}"))
3896+
{
3897+
await command.ExecuteAsync();
3898+
Assert.AreEqual(0, command.ExitStatus, command.Error);
3899+
}
3900+
}
3901+
3902+
#endregion Teardown
3903+
}
3904+
37733905
[TestMethod]
37743906
public void Sftp_ListDirectory()
37753907
{

0 commit comments

Comments
 (0)