Skip to content

Added ExistsAsync and GetAsync to SftpClient #1501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Renci.SshNet/Sftp/ISftpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ internal interface ISftpSession : ISubsystemSession
/// </returns>
SftpFileAttributes RequestLStat(string path);

/// <summary>
/// Asynchronously performs SSH_FXP_LSTAT request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
/// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
/// </returns>
Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken);

/// <summary>
/// Performs SSH_FXP_LSTAT request.
/// </summary>
Expand Down
32 changes: 32 additions & 0 deletions src/Renci.SshNet/Sftp/SftpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,38 @@ public SftpFileAttributes RequestLStat(string path)
return attributes;
}

/// <summary>
/// Asynchronously performs SSH_FXP_LSTAT request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
/// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
/// </returns>
public async Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);

#if NET || NETSTANDARD2_1_OR_GREATER
await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
#else
using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)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);
}
}

/// <summary>
/// Performs SSH_FXP_LSTAT request.
/// </summary>
Expand Down
90 changes: 90 additions & 0 deletions src/Renci.SshNet/SftpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,38 @@ public ISftpFile Get(string path)
return new SftpFile(_sftpSession, fullPath, attributes);
}

/// <summary>
/// Gets reference to remote file or directory.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>
/// A <see cref="Task{ISftpFile}"/> that represents the get operation.
/// The task result contains the reference to <see cref="ISftpFile"/> file object.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
public async Task<ISftpFile> 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);
}

/// <summary>
/// Checks whether file or directory exists.
/// </summary>
Expand Down Expand Up @@ -743,6 +775,64 @@ public bool Exists(string path)
}
}

/// <summary>
/// Checks whether file or directory exists.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>
/// A <see cref="Task{T}"/> that represents the exists operation.
/// The task result contains <see langword="true"/> if directory or file exists; otherwise <see langword="false"/>.
/// </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="SftpPermissionDeniedException">Permission to perform the operation 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>
public async Task<bool> 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;
}
}

/// <summary>
/// Downloads remote file specified by the path into the stream.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
4 changes: 2 additions & 2 deletions test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)>()
Expand Down
Loading