From 375e227c1d7cd6f4813dcc2c6394308ca2bb0b2e Mon Sep 17 00:00:00 2001 From: William Decker Date: Fri, 30 May 2025 07:40:42 -0500 Subject: [PATCH 1/3] Added GetAttributesAsync to SftpClient --- src/Renci.SshNet/ISftpClient.cs | 15 +++++++++++++++ src/Renci.SshNet/SftpClient.cs | 27 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index 04525dcbc..b7ca2f4f8 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -700,6 +700,21 @@ public interface ISftpClient : IBaseClient /// The method was called after the client was disposed. SftpFileAttributes GetAttributes(string path); + /// + /// Gets the of the file on the path. + /// + /// The path to the file. + /// The to observe. + /// + /// A that represents the attribute retrieval operation. + /// The task result contains the of the file on the path. + /// + /// is . + /// Client is not connected. + /// was not found on the remote host. + /// The method was called after the client was disposed. + Task GetAttributesAsync(string path, CancellationToken cancellationToken); + /// /// Returns the date and time the specified file or directory was last accessed. /// diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 90a2f0330..e6d4efc16 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -2094,6 +2094,33 @@ public SftpFileAttributes GetAttributes(string path) return _sftpSession.RequestLStat(fullPath); } + /// + /// Gets the of the file on the path. + /// + /// The path to the file. + /// The to observe. + /// + /// A that represents the attribute retrieval operation. + /// The task result contains the of the file on the path. + /// + /// is . + /// Client is not connected. + /// was not found on the remote host. + /// The method was called after the client was disposed. + public async Task 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); + } + /// /// Sets the specified of the file on the specified path. /// From 459ebc7eac5832a7aa8c78c20428f11be5f5821a Mon Sep 17 00:00:00 2001 From: William Decker Date: Mon, 2 Jun 2025 15:06:30 -0500 Subject: [PATCH 2/3] Adding integration tests + unit test --- .../SftpClientTest.GetAttributes.cs | 47 +++++++++++++++ .../SftpClientTest.GetAttributesAsync.cs | 59 +++++++++++++++++++ .../SftpClientTests.cs | 28 +++++++++ .../Classes/SftpClientTest.GetAttributes.cs | 27 +++++++++ .../SftpClientTest.GetAttributesAsync.cs | 29 +++++++++ 5 files changed, 190 insertions(+) create mode 100644 test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributes.cs create mode 100644 test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributesAsync.cs create mode 100644 test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs create mode 100644 test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributes.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributes.cs new file mode 100644 index 000000000..99c4570a9 --- /dev/null +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributes.cs @@ -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(() => 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(() => 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(); + } + } + } +} diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributesAsync.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributesAsync.cs new file mode 100644 index 000000000..096519eff --- /dev/null +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.GetAttributesAsync.cs @@ -0,0 +1,59 @@ +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + 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(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(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(); + } + } + } +} diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs index 67b3b28c1..1b43bfa11 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs @@ -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); + } } } diff --git a/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs new file mode 100644 index 000000000..cd9a84c5e --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs @@ -0,0 +1,27 @@ +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(() => sftp.GetAttributes(".")); + } + } + + [TestMethod] + public void GetAttributes_Throws_WhenDisposed() + { + var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD); + sftp.Dispose(); + + Assert.ThrowsException(() => sftp.GetAttributes(".")); + } +} diff --git a/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs new file mode 100644 index 000000000..19fbf926c --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs @@ -0,0 +1,29 @@ +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(() => 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(() => sftp.GetAttributesAsync(".", CancellationToken.None)); + } +} From 9ff0ceade405944c86fd05acbdfbca1d25435af2 Mon Sep 17 00:00:00 2001 From: William Decker Date: Tue, 3 Jun 2025 09:42:46 -0500 Subject: [PATCH 3/3] Address warnings in test classes. --- .../Classes/SftpClientTest.GetAttributes.cs | 31 ++++++++++--------- .../SftpClientTest.GetAttributesAsync.cs | 31 ++++++++++--------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs index cd9a84c5e..d91ce0d85 100644 --- a/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs +++ b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributes.cs @@ -1,27 +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 +namespace Renci.SshNet.Tests.Classes { - [TestMethod] - public void GetAttributes_Throws_WhenNotConnected() + public partial class SftpClientTest { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + [TestMethod] + public void GetAttributes_Throws_WhenNotConnected() { - Assert.ThrowsException(() => sftp.GetAttributes(".")); + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + Assert.ThrowsException(() => sftp.GetAttributes(".")); + } } - } - [TestMethod] - public void GetAttributes_Throws_WhenDisposed() - { - var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD); - sftp.Dispose(); + [TestMethod] + public void GetAttributes_Throws_WhenDisposed() + { + var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD); + sftp.Dispose(); - Assert.ThrowsException(() => sftp.GetAttributes(".")); + Assert.ThrowsException(() => sftp.GetAttributes(".")); + } } } diff --git a/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs index 19fbf926c..837307c74 100644 --- a/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs +++ b/test/Renci.SshNet.Tests/Classes/SftpClientTest.GetAttributesAsync.cs @@ -1,29 +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 +namespace Renci.SshNet.Tests.Classes { - [TestMethod] - public async Task GetAttributesAsync_Throws_WhenNotConnected() + public partial class SftpClientTest { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + [TestMethod] + public async Task GetAttributesAsync_Throws_WhenNotConnected() { - await Assert.ThrowsExceptionAsync(() => sftp.GetAttributesAsync(".", CancellationToken.None)); + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + await Assert.ThrowsExceptionAsync(() => sftp.GetAttributesAsync(".", CancellationToken.None)); + } } - } - [TestMethod] - public async Task GetAttributesAsync_Throws_WhenDisposed() - { - var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD); - sftp.Dispose(); + [TestMethod] + public async Task GetAttributesAsync_Throws_WhenDisposed() + { + var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD); + sftp.Dispose(); - await Assert.ThrowsExceptionAsync(() => sftp.GetAttributesAsync(".", CancellationToken.None)); + await Assert.ThrowsExceptionAsync(() => sftp.GetAttributesAsync(".", CancellationToken.None)); + } } }