diff --git a/src/Renci.SshNet/Sftp/ISftpSession.cs b/src/Renci.SshNet/Sftp/ISftpSession.cs
index e49936b7a..ec7e77802 100644
--- a/src/Renci.SshNet/Sftp/ISftpSession.cs
+++ b/src/Renci.SshNet/Sftp/ISftpSession.cs
@@ -116,6 +116,17 @@ internal interface ISftpSession : ISubsystemSession
///
SftpFileAttributes RequestLStat(string path);
+ ///
+ /// Asynchronously performs SSH_FXP_LSTAT request.
+ ///
+ /// The path.
+ /// The token to monitor for cancellation requests.
+ ///
+ /// A task the represents the asynchronous SSH_FXP_LSTAT request. The value of its
+ /// contains the file attributes of the specified path.
+ ///
+ Task RequestLStatAsync(string path, CancellationToken cancellationToken);
+
///
/// Performs SSH_FXP_LSTAT request.
///
diff --git a/src/Renci.SshNet/Sftp/SftpSession.cs b/src/Renci.SshNet/Sftp/SftpSession.cs
index 7f59ac850..a397d66ba 100644
--- a/src/Renci.SshNet/Sftp/SftpSession.cs
+++ b/src/Renci.SshNet/Sftp/SftpSession.cs
@@ -1031,6 +1031,38 @@ public SftpFileAttributes RequestLStat(string path)
return attributes;
}
+ ///
+ /// Asynchronously performs SSH_FXP_LSTAT request.
+ ///
+ /// The path.
+ /// The token to monitor for cancellation requests.
+ ///
+ /// A task the represents the asynchronous SSH_FXP_LSTAT request. The value of its
+ /// contains the file attributes of the specified path.
+ ///
+ public async Task RequestLStatAsync(string path, CancellationToken cancellationToken)
+ {
+ 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 SftpLStatRequest(ProtocolVersion,
+ NextRequestId,
+ path,
+ _encoding,
+ response => tcs.TrySetResult(response.Attributes),
+ response => tcs.TrySetException(GetSftpException(response))));
+
+ return await tcs.Task.ConfigureAwait(false);
+ }
+ }
+
///
/// Performs SSH_FXP_LSTAT request.
///
diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs
index a666ad041..59fe2377e 100644
--- a/src/Renci.SshNet/SftpClient.cs
+++ b/src/Renci.SshNet/SftpClient.cs
@@ -689,6 +689,38 @@ public ISftpFile Get(string path)
return new SftpFile(_sftpSession, fullPath, attributes);
}
+ ///
+ /// Gets reference to remote file or directory.
+ ///
+ /// The path.
+ /// The to observe.
+ ///
+ /// A that represents the get operation.
+ /// The task result contains the reference to file object.
+ ///
+ /// Client is not connected.
+ /// was not found on the remote host.
+ /// is .
+ /// The method was called after the client was disposed.
+ public async Task GetAsync(string path, CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ ThrowHelper.ThrowIfNull(path);
+
+ if (_sftpSession is null)
+ {
+ throw new SshConnectionException("Client not connected.");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+
+ var attributes = await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
+
+ return new SftpFile(_sftpSession, fullPath, attributes);
+ }
+
///
/// Checks whether file or directory exists.
///
@@ -743,6 +775,64 @@ public bool Exists(string path)
}
}
+ ///
+ /// Checks whether file or directory exists.
+ ///
+ /// The path.
+ /// The to observe.
+ ///
+ /// A that represents the exists operation.
+ /// The task result contains if directory or file exists; otherwise .
+ ///
+ /// is or contains only whitespace characters.
+ /// Client is not connected.
+ /// Permission to perform the operation 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 ExistsAsync(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);
+
+ /*
+ * Using SSH_FXP_REALPATH is not an alternative as the SFTP specification has not always
+ * been clear on how the server should respond when the specified path is not present on
+ * the server:
+ *
+ * SSH 1 to 4:
+ * No mention of how the server should respond if the path is not present on the server.
+ *
+ * SSH 5:
+ * The server SHOULD fail the request if the path is not present on the server.
+ *
+ * SSH 6:
+ * Draft 06: The server SHOULD fail the request if the path is not present on the server.
+ * Draft 07 to 13: The server MUST NOT fail the request if the path does not exist.
+ *
+ * Note that SSH 6 (draft 06 and forward) allows for more control options, but we
+ * currently only support up to v3.
+ */
+
+ try
+ {
+ _ = await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+ catch (SftpPathNotFoundException)
+ {
+ return false;
+ }
+ }
+
///
/// Downloads remote file specified by the path into the stream.
///
diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs
index 058442511..ec9fb8c76 100644
--- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs
+++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs
@@ -87,6 +87,85 @@ public void Test_Get_International_File()
Assert.IsFalse(file.IsDirectory);
}
}
+ [TestMethod]
+ [TestCategory("Sftp")]
+ public async Task Test_Get_Root_DirectoryAsync()
+ {
+ using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+ {
+ sftp.Connect();
+ var directory = await sftp.GetAsync("/", default).ConfigureAwait(false);
+
+ Assert.AreEqual("/", directory.FullName);
+ Assert.IsTrue(directory.IsDirectory);
+ Assert.IsFalse(directory.IsRegularFile);
+ }
+ }
+
+ [TestMethod]
+ [TestCategory("Sftp")]
+ [ExpectedException(typeof(SftpPathNotFoundException))]
+ public async Task Test_Get_Invalid_DirectoryAsync()
+ {
+ using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+ {
+ sftp.Connect();
+
+ await sftp.GetAsync("/xyz", default).ConfigureAwait(false);
+ }
+ }
+
+ [TestMethod]
+ [TestCategory("Sftp")]
+ public async Task Test_Get_FileAsync()
+ {
+ using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+ {
+ sftp.Connect();
+
+ sftp.UploadFile(new MemoryStream(), "abc.txt");
+
+ var file = await sftp.GetAsync("abc.txt", default).ConfigureAwait(false);
+
+ Assert.AreEqual("/home/sshnet/abc.txt", file.FullName);
+ Assert.IsTrue(file.IsRegularFile);
+ Assert.IsFalse(file.IsDirectory);
+ }
+ }
+
+ [TestMethod]
+ [TestCategory("Sftp")]
+ [Description("Test passing null to Get.")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task Test_Get_File_NullAsync()
+ {
+ using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+ {
+ sftp.Connect();
+
+ var file = await sftp.GetAsync(null, default).ConfigureAwait(false);
+
+ sftp.Disconnect();
+ }
+ }
+
+ [TestMethod]
+ [TestCategory("Sftp")]
+ public async Task Test_Get_International_FileAsync()
+ {
+ using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+ {
+ sftp.Connect();
+
+ sftp.UploadFile(new MemoryStream(), "test-üöä-");
+
+ var file = await sftp.GetAsync("test-üöä-", default).ConfigureAwait(false);
+
+ Assert.AreEqual("/home/sshnet/test-üöä-", file.FullName);
+ Assert.IsTrue(file.IsRegularFile);
+ Assert.IsFalse(file.IsDirectory);
+ }
+ }
[TestMethod]
[TestCategory("Sftp")]
diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
index a1276e801..49bbf6fae 100644
--- a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
+++ b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
@@ -61,12 +61,12 @@ public async Task Create_directory_with_contents_and_list_it_async()
// Create new directory and check if it exists
_sftpClient.CreateDirectory(testDirectory);
- Assert.IsTrue(_sftpClient.Exists(testDirectory));
+ Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory));
// Upload file and check if it exists
using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
_sftpClient.UploadFile(fileStream, testFilePath);
- Assert.IsTrue(_sftpClient.Exists(testFilePath));
+ Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath));
// Check if ListDirectory works
var expectedFiles = new List<(string FullName, bool IsRegularFile, bool IsDirectory)>()
diff --git a/test/Renci.SshNet.IntegrationTests/SftpTests.cs b/test/Renci.SshNet.IntegrationTests/SftpTests.cs
index 1a2c7ebb9..81e1217e1 100644
--- a/test/Renci.SshNet.IntegrationTests/SftpTests.cs
+++ b/test/Renci.SshNet.IntegrationTests/SftpTests.cs
@@ -3770,6 +3770,138 @@ public void Sftp_Exists()
#endregion Teardown
}
+ [TestMethod]
+ public async Task Sftp_ExistsAsync()
+ {
+ const string remoteHome = "/home/sshnet";
+
+ #region Setup
+
+ using (var client = new SshClient(_connectionInfoFactory.Create()))
+ {
+ client.Connect();
+
+ #region Clean-up
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}")
+ )
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ #endregion Clean-up
+
+ #region Setup
+
+ using (var command = client.CreateCommand($"touch {remoteHome + "/file.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"mkdir {remoteHome + "/directory.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"ln -s {remoteHome + "/file.exists"} {remoteHome + "/symlink.to.file.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"ln -s {remoteHome + "/directory.exists"} {remoteHome + "/symlink.to.directory.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ #endregion Setup
+ }
+
+ #endregion Setup
+
+ #region Assert
+
+ using (var client = new SftpClient(_connectionInfoFactory.Create()))
+ {
+ await client.ConnectAsync(default).ConfigureAwait(false);
+
+ Assert.IsFalse(await client.ExistsAsync(remoteHome + "/DoesNotExist"));
+ Assert.IsTrue(await client.ExistsAsync(remoteHome + "/file.exists"));
+ Assert.IsTrue(await client.ExistsAsync(remoteHome + "/symlink.to.file.exists"));
+ Assert.IsTrue(await client.ExistsAsync(remoteHome + "/directory.exists"));
+ Assert.IsTrue(await client.ExistsAsync(remoteHome + "/symlink.to.directory.exists"));
+ }
+
+ #endregion Assert
+
+ #region Teardown
+
+ using (var client = new SshClient(_connectionInfoFactory.Create()))
+ {
+ client.Connect();
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+
+ using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}"))
+ {
+ await command.ExecuteAsync();
+ Assert.AreEqual(0, command.ExitStatus, command.Error);
+ }
+ }
+
+ #endregion Teardown
+ }
+
[TestMethod]
public void Sftp_ListDirectory()
{