Skip to content

Added GetAttributesAsync to SftpClient #1648

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
Jun 5, 2025
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
15 changes: 15 additions & 0 deletions src/Renci.SshNet/ISftpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,21 @@ public interface ISftpClient : IBaseClient
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
SftpFileAttributes GetAttributes(string path);

/// <summary>
/// Gets the <see cref="SftpFileAttributes"/> of the file on the path.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>
/// A <see cref="Task{SftpFileAttributes}"/> that represents the attribute retrieval operation.
/// The task result contains the <see cref="SftpFileAttributes"/> of the file on the path.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</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="ObjectDisposedException">The method was called after the client was disposed.</exception>
Task<SftpFileAttributes> GetAttributesAsync(string path, CancellationToken cancellationToken);

/// <summary>
/// Returns the date and time the specified file or directory was last accessed.
/// </summary>
Expand Down
27 changes: 27 additions & 0 deletions src/Renci.SshNet/SftpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2094,6 +2094,33 @@ public SftpFileAttributes GetAttributes(string path)
return _sftpSession.RequestLStat(fullPath);
}

/// <summary>
/// Gets the <see cref="SftpFileAttributes"/> of the file on the path.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>
/// A <see cref="Task{SftpFileAttributes}"/> that represents the attribute retrieval operation.
/// The task result contains the <see cref="SftpFileAttributes"/> of the file on the path.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</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="ObjectDisposedException">The method was called after the client was disposed.</exception>
public async Task<SftpFileAttributes> GetAttributesAsync(string path, CancellationToken cancellationToken)
{
CheckDisposed();

if (_sftpSession is null)
{
throw new SshConnectionException("Client not connected.");
}

var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);

return await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Sets the specified <see cref="SftpFileAttributes"/> of the file on the specified path.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Renci.SshNet.Common;

namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
{
public partial class SftpClientTest
{
[TestMethod]
[TestCategory("Sftp")]
public void Test_Sftp_GetAttributes_Not_Exists()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();

Assert.ThrowsException<SftpPathNotFoundException>(() => sftp.GetAttributes("/asdfgh"));
}
}

[TestMethod]
[TestCategory("Sftp")]
public void Test_Sftp_GetAttributes_Null()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();

Assert.ThrowsException<ArgumentNullException>(() => sftp.GetAttributes(null));
}
}

[TestMethod]
[TestCategory("Sftp")]
public void Test_Sftp_GetAttributes_Current()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();

var attributes = sftp.GetAttributes(".");

Assert.IsNotNull(attributes);

sftp.Disconnect();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Renci.SshNet.Common;

namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
{
/// <summary>
/// Implementation of the SSH File Transfer Protocol (SFTP) over SSH.
/// </summary>
public partial class SftpClientTest
{
[TestMethod]
[TestCategory("Sftp")]
public async Task Test_Sftp_GetAttributesAsync_Not_Exists()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(1));

await sftp.ConnectAsync(cts.Token);

await Assert.ThrowsExceptionAsync<SftpPathNotFoundException>(async () => await sftp.GetAttributesAsync("/asdfgh", cts.Token));
}
}

[TestMethod]
[TestCategory("Sftp")]
public async Task Test_Sftp_GetAttributesAsync_Null()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(1));

await sftp.ConnectAsync(cts.Token);

await Assert.ThrowsExceptionAsync<ArgumentNullException>(async () => await sftp.GetAttributesAsync(null, cts.Token));
}
}

[TestMethod]
[TestCategory("Sftp")]
public async Task Test_Sftp_GetAttributesAsync_Current()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(1));

await sftp.ConnectAsync(cts.Token);

var fileAttributes = await sftp.GetAttributesAsync(".", cts.Token);

Assert.IsNotNull(fileAttributes);

sftp.Disconnect();
}
}
}
}
28 changes: 28 additions & 0 deletions test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,33 @@ public async Task Create_file_and_delete_using_DeleteAsync()

Assert.IsFalse(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false));
}

[TestMethod]
public void Create_file_and_use_GetAttributes()
{
var testFileName = "test-file.txt";
var testContent = "file content";

using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
_sftpClient.UploadFile(fileStream, testFileName);

var attributes = _sftpClient.GetAttributes(testFileName);
Assert.IsNotNull(attributes);
Assert.IsTrue(attributes.IsRegularFile);
}

[TestMethod]
public async Task Create_file_and_use_GetAttributesAsync()
{
var testFileName = "test-file.txt";
var testContent = "file content";

using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
await _sftpClient.UploadFileAsync(fileStream, testFileName).ConfigureAwait(false);

var attributes = await _sftpClient.GetAttributesAsync(testFileName, CancellationToken.None).ConfigureAwait(false);
Assert.IsNotNull(attributes);
Assert.IsTrue(attributes.IsRegularFile);
}
}
}
30 changes: 30 additions & 0 deletions test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using Renci.SshNet.Common;
using Renci.SshNet.Tests.Properties;

namespace Renci.SshNet.Tests.Classes
{
public partial class SftpClientTest
{
[TestMethod]
public void GetAttributes_Throws_WhenNotConnected()
{
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
{
Assert.ThrowsException<SshConnectionException>(() => sftp.GetAttributes("."));
}
}

[TestMethod]
public void GetAttributes_Throws_WhenDisposed()
{
var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD);
sftp.Dispose();

Assert.ThrowsException<ObjectDisposedException>(() => sftp.GetAttributes("."));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using Renci.SshNet.Common;
using Renci.SshNet.Tests.Properties;

namespace Renci.SshNet.Tests.Classes
{
public partial class SftpClientTest
{
[TestMethod]
public async Task GetAttributesAsync_Throws_WhenNotConnected()
{
using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD))
{
await Assert.ThrowsExceptionAsync<SshConnectionException>(() => sftp.GetAttributesAsync(".", CancellationToken.None));
}
}

[TestMethod]
public async Task GetAttributesAsync_Throws_WhenDisposed()
{
var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD);
sftp.Dispose();

await Assert.ThrowsExceptionAsync<ObjectDisposedException>(() => sftp.GetAttributesAsync(".", CancellationToken.None));
}
}
}
Loading