From 1b8ab35aadf48ebd352f5a7483dd2d237fa048b5 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Wed, 23 Apr 2025 11:56:33 -0500 Subject: [PATCH 01/23] add interface methods --- src/Renci.SshNet/ISftpClient.cs | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index 2c37bbcbe..eec3f90ce 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -573,6 +573,25 @@ public interface ISftpClient : IBaseClient /// void DownloadFile(string path, Stream output, Action? downloadCallback = null); + /// + /// Asynchronously downloads remote file specified by the path into the stream. + /// + /// File to download. + /// Stream to write the file into. + /// The to observe. + /// A that represents the asynchronous download operation. + /// is . + /// 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. + /// was not found on the remote host./// + /// A SSH error where is the message from the remote host. + /// The method was called after the client was disposed. + /// + /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. + /// + Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default); + /// /// Ends an asynchronous file downloading into the stream. /// @@ -1104,6 +1123,37 @@ public interface ISftpClient : IBaseClient /// void UploadFile(Stream input, string path, bool canOverride, Action? uploadCallback = null); + /// + /// Asynchronously uploads stream into remote file. + /// + /// Data input stream. + /// Remote file path. + /// The to observe. + /// A that represents the asynchronous upload operation. + /// is . + /// is or contains only whitespace characters. + /// Client is not connected. + /// Permission to upload the file 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. + Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default); + + /// + /// Asynchronously uploads stream into remote file. + /// + /// Data input stream. + /// Remote file path. + /// if set to then existing file will be overwritten. + /// The to observe. + /// A that represents the asynchronous upload operation. + /// is . + /// is or contains only whitespace characters. + /// Client is not connected. + /// Permission to upload the file 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. + Task UploadFileAsync(Stream input, string path, bool canOverride, CancellationToken cancellationToken = default); + /// /// Writes the specified byte array to the specified file, and closes the file. /// From e4cf86f9db14d6917690d536bd33f6a35c289437 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Wed, 23 Apr 2025 14:58:31 -0500 Subject: [PATCH 02/23] add internal file methods --- src/Renci.SshNet/Sftp/SftpFileStream.cs | 2 +- src/Renci.SshNet/SftpClient.cs | 56 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/Renci.SshNet/Sftp/SftpFileStream.cs b/src/Renci.SshNet/Sftp/SftpFileStream.cs index 2965f86e7..048da58a3 100644 --- a/src/Renci.SshNet/Sftp/SftpFileStream.cs +++ b/src/Renci.SshNet/Sftp/SftpFileStream.cs @@ -191,7 +191,7 @@ public TimeSpan Timeout } } - private SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position) + internal SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position) { Timeout = TimeSpan.FromSeconds(30); Name = path; diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 7cf5e62a3..7e29f964e 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -2433,6 +2433,34 @@ private void InternalDownloadFile(string path, Stream output, SftpDownloadAsyncR } } + private async Task InternalDownloadFileAsync(string path, Stream output, CancellationToken cancellationToken) + { + ThrowHelper.ThrowIfNull(output); + ThrowHelper.ThrowIfNullOrWhiteSpace(path); + + if (_sftpSession is null) + { + throw new SshConnectionException("Client not connected."); + } + + var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); + + var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Read, cancellationToken).ConfigureAwait(false); + try + { + var bufferSize = (int)_sftpSession.CalculateOptimalReadLength(_bufferSize); + + using (var input = new SftpFileStream(_sftpSession, fullPath, FileAccess.Read, bufferSize, handle, 0L)) + { + await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); + } + } + finally + { + await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false); + } + } + /// /// Internals the upload file. /// @@ -2515,6 +2543,34 @@ private void InternalUploadFile(Stream input, string path, Flags flags, SftpUplo responseReceivedWaitHandle.Dispose(); } + private async Task InternalUploadFileAsync(Stream input, string path, Flags flags, CancellationToken cancellationToken) + { + ThrowHelper.ThrowIfNull(input); + ThrowHelper.ThrowIfNullOrWhiteSpace(path); + + if (_sftpSession is null) + { + throw new SshConnectionException("Client not connected."); + } + + var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); + + var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Write | flags, cancellationToken).ConfigureAwait(false); + try + { + var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); + + using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, bufferSize, handle, 0L)) + { + await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); + } + } + finally + { + await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false); + } + } + /// /// Called when client is connected to the server. /// From dba2c1bafa2cf15cbc3497ad12703888148536b2 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 09:08:23 -0500 Subject: [PATCH 03/23] impl interface methods --- src/Renci.SshNet/SftpClient.cs | 74 ++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 7e29f964e..59ee24d40 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -916,6 +916,30 @@ public void DownloadFile(string path, Stream output, Action? downloadCall InternalDownloadFile(path, output, asyncResult: null, downloadCallback); } + /// + /// Asynchronously downloads remote file specified by the path into the stream. + /// + /// File to download. + /// Stream to write the file into. + /// The to observe. + /// A that represents the asynchronous download operation. + /// is . + /// 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. + /// was not found on the remote host./// + /// A SSH error where is the message from the remote host. + /// The method was called after the client was disposed. + /// + /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. + /// + public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default) + { + CheckDisposed(); + + return InternalDownloadFileAsync(path, output, cancellationToken); + } + /// /// Begins an asynchronous file downloading into the stream. /// @@ -1077,6 +1101,56 @@ public void UploadFile(Stream input, string path, bool canOverride, Action + /// Asynchronously uploads stream into remote file. + /// + /// Data input stream. + /// Remote file path. + /// The to observe. + /// A that represents the asynchronous upload operation. + /// is . + /// is or contains only whitespace characters. + /// Client is not connected. + /// Permission to upload the file 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 Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default) + { + return UploadFileAsync(input, path, canOverride: true, cancellationToken: cancellationToken); + } + + /// + /// Asynchronously uploads stream into remote file. + /// + /// Data input stream. + /// Remote file path. + /// if set to then existing file will be overwritten. + /// The to observe. + /// A that represents the asynchronous upload operation. + /// is . + /// is or contains only whitespace characters. + /// Client is not connected. + /// Permission to upload the file 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 Task UploadFileAsync(Stream input, string path, bool canOverride, CancellationToken cancellationToken = default) + { + CheckDisposed(); + + var flags = Flags.Write | Flags.Truncate; + + if (canOverride) + { + flags |= Flags.CreateNewOrOpen; + } + else + { + flags |= Flags.CreateNew; + } + + return InternalUploadFileAsync(input, path, flags, cancellationToken); + } + /// /// Begins an asynchronous uploading the stream into remote file. /// From 9af76edca6c5b3594efd52905c71dd554b504735 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 09:20:06 -0500 Subject: [PATCH 04/23] tweak buffer size usage --- src/Renci.SshNet/SftpClient.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 59ee24d40..322675625 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -2522,10 +2522,10 @@ private async Task InternalDownloadFileAsync(string path, Stream output, Cancell var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Read, cancellationToken).ConfigureAwait(false); try { - var bufferSize = (int)_sftpSession.CalculateOptimalReadLength(_bufferSize); - - using (var input = new SftpFileStream(_sftpSession, fullPath, FileAccess.Read, bufferSize, handle, 0L)) + using (var input = new SftpFileStream(_sftpSession, fullPath, FileAccess.Read, (int)_bufferSize, handle, 0L)) { + var bufferSize = (int)_sftpSession.CalculateOptimalReadLength(_bufferSize); + await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); } } @@ -2632,10 +2632,10 @@ private async Task InternalUploadFileAsync(Stream input, string path, Flags flag var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Write | flags, cancellationToken).ConfigureAwait(false); try { - var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); - - using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, bufferSize, handle, 0L)) + using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, (int)_bufferSize, handle, 0L)) { + var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); + await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); } } From 436662592008954b332d3c6495b649f06f950f75 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 09:27:44 -0500 Subject: [PATCH 05/23] swap tests with async upload --- .../OldIntegrationTests/SftpFileTest.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs index 34819a6e6..344819b33 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs @@ -88,7 +88,7 @@ public async Task Test_Get_Root_DirectoryAsync() { using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { - sftp.Connect(); + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); var directory = await sftp.GetAsync("/", default).ConfigureAwait(false); Assert.AreEqual("/", directory.FullName); @@ -103,7 +103,7 @@ public async Task Test_Get_Invalid_DirectoryAsync() { using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { - sftp.Connect(); + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); await Assert.ThrowsExceptionAsync(() => sftp.GetAsync("/xyz", default)); } @@ -115,9 +115,9 @@ public async Task Test_Get_FileAsync() { using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { - sftp.Connect(); + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); - sftp.UploadFile(new MemoryStream(), "abc.txt"); + await sftp.UploadFileAsync(new MemoryStream(), "abc.txt").ConfigureAwait(false); var file = await sftp.GetAsync("abc.txt", default).ConfigureAwait(false); @@ -133,7 +133,7 @@ public async Task Test_Get_File_NullAsync() { using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { - sftp.Connect(); + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); await Assert.ThrowsExceptionAsync(() => sftp.GetAsync(null, default)); } @@ -145,9 +145,9 @@ public async Task Test_Get_International_FileAsync() { using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { - sftp.Connect(); + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); - sftp.UploadFile(new MemoryStream(), "test-üöä-"); + await sftp.UploadFileAsync(new MemoryStream(), "test-üöä-").ConfigureAwait(false); var file = await sftp.GetAsync("test-üöä-", default).ConfigureAwait(false); From 8ae8875d30153e0a09bed74c4cddad028f96a927 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 09:35:17 -0500 Subject: [PATCH 06/23] swap more upload file references --- test/Renci.SshNet.IntegrationTests/SftpClientTests.cs | 6 +++--- test/Renci.SshNet.IntegrationTests/SftpTests.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs index 52c4223f4..90c6897b0 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs @@ -65,7 +65,7 @@ public async Task Create_directory_with_contents_and_list_it_async() // Upload file and check if it exists using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent)); - _sftpClient.UploadFile(fileStream, testFilePath); + await _sftpClient.UploadFileAsync(fileStream, testFilePath); Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath)); // Check if ListDirectory works @@ -123,7 +123,7 @@ public async Task Create_directory_with_contents_and_delete_contents_then_direct // Upload file and check if it exists using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent)); - _sftpClient.UploadFile(fileStream, testFilePath); + await _sftpClient.UploadFileAsync(fileStream, testFilePath); Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath).ConfigureAwait(false)); await _sftpClient.DeleteFileAsync(testFilePath, CancellationToken.None).ConfigureAwait(false); @@ -158,7 +158,7 @@ public async Task Create_file_and_delete_using_DeleteAsync() // Upload file and check if it exists using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent)); - _sftpClient.UploadFile(fileStream, testFileName); + await _sftpClient.UploadFileAsync(fileStream, testFileName); Assert.IsTrue(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false)); await _sftpClient.DeleteAsync(testFileName, CancellationToken.None).ConfigureAwait(false); diff --git a/test/Renci.SshNet.IntegrationTests/SftpTests.cs b/test/Renci.SshNet.IntegrationTests/SftpTests.cs index 8a3c229f1..671e6cdd0 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpTests.cs @@ -4115,7 +4115,7 @@ public async Task Sftp_ChangeDirectory_DirectoryExistsAsync() { uploadStream.Position = 0; - client.UploadFile(uploadStream, "gert.txt"); + await client.UploadFileAsync(uploadStream, "gert.txt"); uploadStream.Position = 0; From 7516e6b557da0a8da6990c757f4d671bd200f149 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 11:36:48 -0500 Subject: [PATCH 07/23] add async download tests --- .../SftpClientTest.Download.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs index d1475c5f3..d6837780d 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs @@ -31,6 +31,30 @@ public void Test_Sftp_Download_File_Not_Exists() } } + [TestMethod] + [TestCategory("Sftp")] + public async Task Test_Sftp_DownloadAsync_Forbidden() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, AdminUser.UserName, AdminUser.Password)) + { + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); + + await Assert.ThrowsExceptionAsync(() => sftp.DownloadFileAsync("/root/.profile", Stream.Null)); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public async Task Test_Sftp_DownloadAsync_File_Not_Exists() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); + + await Assert.ThrowsExceptionAsync(() => sftp.DownloadFileAsync("/xxx/eee/yyy", Stream.Null)); + } + } + [TestMethod] [TestCategory("Sftp")] [Description("Test passing null to BeginDownloadFile")] From 9be6f64f15cd21946bb689a0a9cfffa5a19e1361 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 11:37:10 -0500 Subject: [PATCH 08/23] add upload/download integration async test --- .../SftpClientTest.Upload.cs | 43 +++++++++++++++++++ .../OldIntegrationTests/SftpClientTest.cs | 21 +++++++++ .../TestsFixtures/IntegrationTestBase.cs | 20 +++++++++ 3 files changed, 84 insertions(+) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs index e5cd91400..05d231a54 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs @@ -51,6 +51,49 @@ public void Test_Sftp_Upload_And_Download_1MB_File() } } + [TestMethod] + [TestCategory("Sftp")] + public async Task Test_Sftp_Upload_And_Download_Async_1MB_File() + { + RemoveAllFiles(); + + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); + + var uploadedFileName = Path.GetTempFileName(); + var remoteFileName = Path.GetRandomFileName(); + + await CreateTestFileAsync(uploadedFileName, 1, CancellationToken.None).ConfigureAwait(false); + + // Calculate has value + var uploadedHash = await CalculateMD5Async(uploadedFileName, CancellationToken.None).ConfigureAwait(false); + + using (var file = File.OpenRead(uploadedFileName)) + { + await sftp.UploadFileAsync(file, remoteFileName).ConfigureAwait(false); + } + + var downloadedFileName = Path.GetTempFileName(); + + using (var file = File.OpenWrite(downloadedFileName)) + { + await sftp.DownloadFileAsync(remoteFileName, file).ConfigureAwait(false); + } + + var downloadedHash = await CalculateMD5Async(downloadedFileName, CancellationToken.None).ConfigureAwait(false); + + await sftp.DeleteFileAsync(remoteFileName, CancellationToken.None).ConfigureAwait(false); + + File.Delete(uploadedFileName); + File.Delete(downloadedFileName); + + sftp.Disconnect(); + + Assert.AreEqual(uploadedHash, downloadedHash); + } + } + [TestMethod] [TestCategory("Sftp")] public void Test_Sftp_Upload_Forbidden() diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs index 3c49e60fa..d32324269 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs @@ -31,6 +31,27 @@ protected static string CalculateMD5(string fileName) } } + protected static async Task CalculateMD5Async(string fileName, CancellationToken cancellationToken) + { + using (FileStream file = new FileStream(fileName, FileMode.Open)) + { + byte[] hash; + using (var md5 = MD5.Create()) + { + hash = await md5.ComputeHashAsync(file, cancellationToken).ConfigureAwait(false); + } + + file.Close(); + + StringBuilder sb = new StringBuilder(); + for (var i = 0; i < hash.Length; i++) + { + sb.Append(hash[i].ToString("x2")); + } + return sb.ToString(); + } + } + private void RemoveAllFiles() { using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs index 6838e54d7..6c07ef781 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs +++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs @@ -81,5 +81,25 @@ protected void CreateTestFile(string fileName, int size) } } } + + /// + /// Creates the test file. + /// + /// Name of the file. + /// Size in megabytes. + /// The to observe. + protected async Task CreateTestFileAsync(string fileName, int size, CancellationToken cancellationToken) + { + using (var testFile = File.Create(fileName)) + { + var random = new Random(); + for (int i = 0; i < 1024 * size; i++) + { + var buffer = new byte[1024]; + random.NextBytes(buffer); + await testFile.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + } + } + } } } From d5f220156c7717abc24a7c25bfcbb13f6192314c Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 12:06:50 -0500 Subject: [PATCH 09/23] check if net48 --- .../OldIntegrationTests/SftpClientTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs index d32324269..912a3b521 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs @@ -38,7 +38,11 @@ protected static async Task CalculateMD5Async(string fileName, Cancellat byte[] hash; using (var md5 = MD5.Create()) { +#if NET48 + hash = md5.ComputeHash(file); +#else hash = await md5.ComputeHashAsync(file, cancellationToken).ConfigureAwait(false); +#endif } file.Close(); From 4c4751a7a7d94cb345776b912e02d3ea1d093ff2 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 12:19:31 -0500 Subject: [PATCH 10/23] silence not await warning --- .../OldIntegrationTests/SftpClientTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs index 912a3b521..88688c92b 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs @@ -40,6 +40,7 @@ protected static async Task CalculateMD5Async(string fileName, Cancellat { #if NET48 hash = md5.ComputeHash(file); + await Task.CompletedTask.ConfigureAwait(false); #else hash = await md5.ComputeHashAsync(file, cancellationToken).ConfigureAwait(false); #endif From ad049c54c6d5e7697ba23945ce2c8cf960ff8ca8 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 17:14:34 -0500 Subject: [PATCH 11/23] try tweaking test init --- .../SftpClientTests.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs index 90c6897b0..c719182ee 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs @@ -6,14 +6,26 @@ namespace Renci.SshNet.IntegrationTests /// The SFTP client integration tests /// [TestClass] - public class SftpClientTests : IntegrationTestBase, IDisposable + public class SftpClientTests : IntegrationTestBase { private readonly SftpClient _sftpClient; public SftpClientTests() { _sftpClient = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password); - _sftpClient.Connect(); + } + + [TestInitialize] + public async Task InitializeAsync() + { + await _sftpClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false); + } + + [TestCleanup] + public void Cleanup() + { + _sftpClient.Disconnect(); + _sftpClient.Dispose(); } [TestMethod] @@ -165,11 +177,5 @@ public async Task Create_file_and_delete_using_DeleteAsync() Assert.IsFalse(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false)); } - - public void Dispose() - { - _sftpClient.Disconnect(); - _sftpClient.Dispose(); - } } } From 8bb291821e25d52b7af467e6299f57b3554b92d5 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Thu, 24 Apr 2025 17:18:09 -0500 Subject: [PATCH 12/23] remove request close from upload --- src/Renci.SshNet/SftpClient.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 322675625..ea30e6228 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -2630,18 +2630,12 @@ private async Task InternalUploadFileAsync(Stream input, string path, Flags flag var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Write | flags, cancellationToken).ConfigureAwait(false); - try - { - using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, (int)_bufferSize, handle, 0L)) - { - var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); - await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); - } - } - finally + using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, (int)_bufferSize, handle, 0L)) { - await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false); + var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); + + await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); } } From f5075857848a4b50ceb7798350cfce13f0d46fb0 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 25 Apr 2025 10:15:26 -0500 Subject: [PATCH 13/23] dispose already closes, remove dup call --- src/Renci.SshNet/SftpClient.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index ea30e6228..a1982b77b 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -2518,20 +2518,12 @@ private async Task InternalDownloadFileAsync(string path, Stream output, Cancell } var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); - var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Read, cancellationToken).ConfigureAwait(false); - try - { - using (var input = new SftpFileStream(_sftpSession, fullPath, FileAccess.Read, (int)_bufferSize, handle, 0L)) - { - var bufferSize = (int)_sftpSession.CalculateOptimalReadLength(_bufferSize); - await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); - } - } - finally + using (var input = new SftpFileStream(_sftpSession, fullPath, FileAccess.Read, (int)_bufferSize, handle, 0L)) { - await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false); + var bufferSize = (int)_sftpSession.CalculateOptimalReadLength(_bufferSize); + await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); } } @@ -2628,13 +2620,11 @@ private async Task InternalUploadFileAsync(Stream input, string path, Flags flag } var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); - var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Write | flags, cancellationToken).ConfigureAwait(false); using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, (int)_bufferSize, handle, 0L)) { var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); - await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); } } From f63b8859cfcc9bb0397a80b5be7de7381b92f71d Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 2 May 2025 09:14:46 -0500 Subject: [PATCH 14/23] remove extra upload overload --- src/Renci.SshNet/ISftpClient.cs | 16 ---------------- src/Renci.SshNet/SftpClient.cs | 30 +----------------------------- 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index eec3f90ce..26ef3f088 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -1138,22 +1138,6 @@ public interface ISftpClient : IBaseClient /// The method was called after the client was disposed. Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default); - /// - /// Asynchronously uploads stream into remote file. - /// - /// Data input stream. - /// Remote file path. - /// if set to then existing file will be overwritten. - /// The to observe. - /// A that represents the asynchronous upload operation. - /// is . - /// is or contains only whitespace characters. - /// Client is not connected. - /// Permission to upload the file 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. - Task UploadFileAsync(Stream input, string path, bool canOverride, CancellationToken cancellationToken = default); - /// /// Writes the specified byte array to the specified file, and closes the file. /// diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index a1982b77b..0bb068f9e 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -1115,38 +1115,10 @@ public void UploadFile(Stream input, string path, bool canOverride, ActionA SSH error where is the message from the remote host. /// The method was called after the client was disposed. public Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default) - { - return UploadFileAsync(input, path, canOverride: true, cancellationToken: cancellationToken); - } - - /// - /// Asynchronously uploads stream into remote file. - /// - /// Data input stream. - /// Remote file path. - /// if set to then existing file will be overwritten. - /// The to observe. - /// A that represents the asynchronous upload operation. - /// is . - /// is or contains only whitespace characters. - /// Client is not connected. - /// Permission to upload the file 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 Task UploadFileAsync(Stream input, string path, bool canOverride, CancellationToken cancellationToken = default) { CheckDisposed(); - var flags = Flags.Write | Flags.Truncate; - - if (canOverride) - { - flags |= Flags.CreateNewOrOpen; - } - else - { - flags |= Flags.CreateNew; - } + var flags = Flags.Write | Flags.Truncate | Flags.CreateNewOrOpen; return InternalUploadFileAsync(input, path, flags, cancellationToken); } From 599079835f9b9801bbe1cab436ae1e8a9a0117ae Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 2 May 2025 09:15:42 -0500 Subject: [PATCH 15/23] inherit doc --- src/Renci.SshNet/SftpClient.cs | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 0bb068f9e..840983196 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -916,23 +916,7 @@ public void DownloadFile(string path, Stream output, Action? downloadCall InternalDownloadFile(path, output, asyncResult: null, downloadCallback); } - /// - /// Asynchronously downloads remote file specified by the path into the stream. - /// - /// File to download. - /// Stream to write the file into. - /// The to observe. - /// A that represents the asynchronous download operation. - /// is . - /// 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. - /// was not found on the remote host./// - /// A SSH error where is the message from the remote host. - /// The method was called after the client was disposed. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// + /// public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default) { CheckDisposed(); @@ -1101,19 +1085,7 @@ public void UploadFile(Stream input, string path, bool canOverride, Action - /// Asynchronously uploads stream into remote file. - /// - /// Data input stream. - /// Remote file path. - /// The to observe. - /// A that represents the asynchronous upload operation. - /// is . - /// is or contains only whitespace characters. - /// Client is not connected. - /// Permission to upload the file 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 Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default) { CheckDisposed(); From 5cd16b718e5497946bb3b58d716612d0725c5aff Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 2 May 2025 09:19:30 -0500 Subject: [PATCH 16/23] configure await --- test/Renci.SshNet.IntegrationTests/SftpClientTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs index c719182ee..67b3b28c1 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs @@ -77,7 +77,7 @@ public async Task Create_directory_with_contents_and_list_it_async() // Upload file and check if it exists using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent)); - await _sftpClient.UploadFileAsync(fileStream, testFilePath); + await _sftpClient.UploadFileAsync(fileStream, testFilePath).ConfigureAwait(false); Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath)); // Check if ListDirectory works @@ -130,12 +130,12 @@ public async Task Create_directory_with_contents_and_delete_contents_then_direct var testContent = "file content"; // Create new directory and check if it exists - await _sftpClient.CreateDirectoryAsync(testDirectory); + await _sftpClient.CreateDirectoryAsync(testDirectory).ConfigureAwait(false); Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false)); // Upload file and check if it exists using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent)); - await _sftpClient.UploadFileAsync(fileStream, testFilePath); + await _sftpClient.UploadFileAsync(fileStream, testFilePath).ConfigureAwait(false); Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath).ConfigureAwait(false)); await _sftpClient.DeleteFileAsync(testFilePath, CancellationToken.None).ConfigureAwait(false); @@ -154,7 +154,7 @@ public async Task Create_directory_and_delete_it_using_DeleteAsync() var testDirectory = "/home/sshnet/sshnet-test"; // Create new directory and check if it exists - await _sftpClient.CreateDirectoryAsync(testDirectory); + await _sftpClient.CreateDirectoryAsync(testDirectory).ConfigureAwait(false); Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false)); await _sftpClient.DeleteAsync(testDirectory, CancellationToken.None).ConfigureAwait(false); @@ -170,7 +170,7 @@ public async Task Create_file_and_delete_using_DeleteAsync() // Upload file and check if it exists using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent)); - await _sftpClient.UploadFileAsync(fileStream, testFileName); + await _sftpClient.UploadFileAsync(fileStream, testFileName).ConfigureAwait(false); Assert.IsTrue(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false)); await _sftpClient.DeleteAsync(testFileName, CancellationToken.None).ConfigureAwait(false); From fbc808128d1a12b4f036bf394d3f9cf334b49849 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 2 May 2025 09:23:33 -0500 Subject: [PATCH 17/23] remove excess util functions --- .../SftpClientTest.Upload.cs | 6 ++--- .../OldIntegrationTests/SftpClientTest.cs | 26 ------------------- .../TestsFixtures/IntegrationTestBase.cs | 20 -------------- 3 files changed, 3 insertions(+), 49 deletions(-) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs index 05d231a54..227170b68 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs @@ -64,10 +64,10 @@ public async Task Test_Sftp_Upload_And_Download_Async_1MB_File() var uploadedFileName = Path.GetTempFileName(); var remoteFileName = Path.GetRandomFileName(); - await CreateTestFileAsync(uploadedFileName, 1, CancellationToken.None).ConfigureAwait(false); + CreateTestFile(uploadedFileName, 1); // Calculate has value - var uploadedHash = await CalculateMD5Async(uploadedFileName, CancellationToken.None).ConfigureAwait(false); + var uploadedHash = CalculateMD5(uploadedFileName); using (var file = File.OpenRead(uploadedFileName)) { @@ -81,7 +81,7 @@ public async Task Test_Sftp_Upload_And_Download_Async_1MB_File() await sftp.DownloadFileAsync(remoteFileName, file).ConfigureAwait(false); } - var downloadedHash = await CalculateMD5Async(downloadedFileName, CancellationToken.None).ConfigureAwait(false); + var downloadedHash = CalculateMD5(downloadedFileName); await sftp.DeleteFileAsync(remoteFileName, CancellationToken.None).ConfigureAwait(false); diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs index 88688c92b..3c49e60fa 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs @@ -31,32 +31,6 @@ protected static string CalculateMD5(string fileName) } } - protected static async Task CalculateMD5Async(string fileName, CancellationToken cancellationToken) - { - using (FileStream file = new FileStream(fileName, FileMode.Open)) - { - byte[] hash; - using (var md5 = MD5.Create()) - { -#if NET48 - hash = md5.ComputeHash(file); - await Task.CompletedTask.ConfigureAwait(false); -#else - hash = await md5.ComputeHashAsync(file, cancellationToken).ConfigureAwait(false); -#endif - } - - file.Close(); - - StringBuilder sb = new StringBuilder(); - for (var i = 0; i < hash.Length; i++) - { - sb.Append(hash[i].ToString("x2")); - } - return sb.ToString(); - } - } - private void RemoveAllFiles() { using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs index 6c07ef781..6838e54d7 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs +++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs @@ -81,25 +81,5 @@ protected void CreateTestFile(string fileName, int size) } } } - - /// - /// Creates the test file. - /// - /// Name of the file. - /// Size in megabytes. - /// The to observe. - protected async Task CreateTestFileAsync(string fileName, int size, CancellationToken cancellationToken) - { - using (var testFile = File.Create(fileName)) - { - var random = new Random(); - for (int i = 0; i < 1024 * size; i++) - { - var buffer = new byte[1024]; - random.NextBytes(buffer); - await testFile.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - } - } - } } } From b121189c66b961dba62f0d83b87a3382b218d9c2 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 2 May 2025 09:24:42 -0500 Subject: [PATCH 18/23] add cancel throws --- src/Renci.SshNet/SftpClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 840983196..fe04f1ce0 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -2461,6 +2461,8 @@ private async Task InternalDownloadFileAsync(string path, Stream output, Cancell throw new SshConnectionException("Client not connected."); } + cancellationToken.ThrowIfCancellationRequested(); + var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Read, cancellationToken).ConfigureAwait(false); @@ -2563,6 +2565,8 @@ private async Task InternalUploadFileAsync(Stream input, string path, Flags flag throw new SshConnectionException("Client not connected."); } + cancellationToken.ThrowIfCancellationRequested(); + var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Write | flags, cancellationToken).ConfigureAwait(false); From b949d4ec3bb79f311caee0dbb1940f91e448935b Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 2 May 2025 10:41:27 -0500 Subject: [PATCH 19/23] add cancellation tests --- .../SftpClientTest.Download.cs | 14 ++++++++++++ .../SftpClientTest.Upload.cs | 22 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs index d6837780d..1b87ecfbd 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs @@ -55,6 +55,20 @@ public async Task Test_Sftp_DownloadAsync_File_Not_Exists() } } + [TestMethod] + [TestCategory("Sftp")] + public async Task Test_Sftp_DownloadAsync_Cancellation_Requested() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false); + + var cancelledToken = new CancellationToken(true); + + await Assert.ThrowsExceptionAsync(() => sftp.DownloadFileAsync("/xxx/eee/yyy", Stream.Null, cancelledToken)); + } + } + [TestMethod] [TestCategory("Sftp")] [Description("Test passing null to BeginDownloadFile")] diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs index 227170b68..a3c626fa6 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs @@ -116,6 +116,28 @@ public void Test_Sftp_Upload_Forbidden() } } + [TestMethod] + [TestCategory("Sftp")] + public async Task Test_Sftp_UploadAsync_Cancellation_Requested() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + await sftp.ConnectAsync(CancellationToken.None); + + var uploadedFileName = Path.GetTempFileName(); + var remoteFileName = "/root/1"; + + CreateTestFile(uploadedFileName, 1); + + var cancelledToken = new CancellationToken(true); + + using (var file = File.OpenRead(uploadedFileName)) + { + await Assert.ThrowsAsync(() => sftp.UploadFileAsync(file, remoteFileName, cancelledToken)); + } + } + } + [TestMethod] [TestCategory("Sftp")] public void Test_Sftp_Multiple_Async_Upload_And_Download_10Files_5MB_Each() From d5b6333e0292ff581662e59da4623b9eab1f2fda Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Fri, 2 May 2025 10:47:56 -0500 Subject: [PATCH 20/23] missed one configure await --- test/Renci.SshNet.IntegrationTests/SftpTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Renci.SshNet.IntegrationTests/SftpTests.cs b/test/Renci.SshNet.IntegrationTests/SftpTests.cs index 671e6cdd0..83275e0e3 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpTests.cs @@ -4115,7 +4115,7 @@ public async Task Sftp_ChangeDirectory_DirectoryExistsAsync() { uploadStream.Position = 0; - await client.UploadFileAsync(uploadStream, "gert.txt"); + await client.UploadFileAsync(uploadStream, "gert.txt").ConfigureAwait(false); uploadStream.Position = 0; From 181c9d4f78c763e9c09ad3061ae31582bad43121 Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Mon, 5 May 2025 10:44:41 -0500 Subject: [PATCH 21/23] use default buffer size --- src/Renci.SshNet/SftpClient.cs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index fe04f1ce0..dcb209515 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -1090,9 +1090,7 @@ public Task UploadFileAsync(Stream input, string path, CancellationToken cancell { CheckDisposed(); - var flags = Flags.Write | Flags.Truncate | Flags.CreateNewOrOpen; - - return InternalUploadFileAsync(input, path, flags, cancellationToken); + return InternalUploadFileAsync(input, path, FileMode.Create, cancellationToken); } /// @@ -2464,12 +2462,11 @@ private async Task InternalDownloadFileAsync(string path, Stream output, Cancell cancellationToken.ThrowIfCancellationRequested(); var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); - var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Read, cancellationToken).ConfigureAwait(false); + var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, FileMode.Open, FileAccess.Read, (int)_bufferSize, cancellationToken); - using (var input = new SftpFileStream(_sftpSession, fullPath, FileAccess.Read, (int)_bufferSize, handle, 0L)) + using (var input = await openStreamTask.ConfigureAwait(false)) { - var bufferSize = (int)_sftpSession.CalculateOptimalReadLength(_bufferSize); - await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); + await input.CopyToAsync(output, 81920, cancellationToken).ConfigureAwait(false); } } @@ -2555,7 +2552,7 @@ private void InternalUploadFile(Stream input, string path, Flags flags, SftpUplo responseReceivedWaitHandle.Dispose(); } - private async Task InternalUploadFileAsync(Stream input, string path, Flags flags, CancellationToken cancellationToken) + private async Task InternalUploadFileAsync(Stream input, string path, FileMode fileMode, CancellationToken cancellationToken) { ThrowHelper.ThrowIfNull(input); ThrowHelper.ThrowIfNullOrWhiteSpace(path); @@ -2568,12 +2565,11 @@ private async Task InternalUploadFileAsync(Stream input, string path, Flags flag cancellationToken.ThrowIfCancellationRequested(); var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); - var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Write | flags, cancellationToken).ConfigureAwait(false); + var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, fileMode, FileAccess.Write, (int)_bufferSize, cancellationToken); - using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, (int)_bufferSize, handle, 0L)) + using (var output = await openStreamTask.ConfigureAwait(false)) { - var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle); - await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false); + await input.CopyToAsync(output, 81920, cancellationToken).ConfigureAwait(false); } } From 342a8422c48780b63df4bb52f0554a55ca0bde1a Mon Sep 17 00:00:00 2001 From: Noah Dela Rosa Date: Mon, 5 May 2025 11:16:43 -0500 Subject: [PATCH 22/23] private ctor --- src/Renci.SshNet/Sftp/SftpFileStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Renci.SshNet/Sftp/SftpFileStream.cs b/src/Renci.SshNet/Sftp/SftpFileStream.cs index 048da58a3..2965f86e7 100644 --- a/src/Renci.SshNet/Sftp/SftpFileStream.cs +++ b/src/Renci.SshNet/Sftp/SftpFileStream.cs @@ -191,7 +191,7 @@ public TimeSpan Timeout } } - internal SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position) + private SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position) { Timeout = TimeSpan.FromSeconds(30); Name = path; From bde04ccde119699859e03f957fa1fc12651ce2c9 Mon Sep 17 00:00:00 2001 From: Rob Hague Date: Mon, 5 May 2025 22:41:07 +0200 Subject: [PATCH 23/23] docs --- src/Renci.SshNet/ISftpClient.cs | 88 ++++++++++++++------------------- src/Renci.SshNet/SftpClient.cs | 56 +++------------------ 2 files changed, 44 insertions(+), 100 deletions(-) diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index 26ef3f088..04525dcbc 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -556,40 +556,34 @@ public interface ISftpClient : IBaseClient Task DeleteFileAsync(string path, CancellationToken cancellationToken); /// - /// Downloads remote file specified by the path into the stream. + /// Downloads a remote file into a . /// - /// File to download. - /// Stream to write the file into. + /// The path to the remote file. + /// The to write the file into. /// The download callback. - /// is . - /// is or contains only whitespace characters. + /// or is . + /// is empty 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. - /// was not found on the remote host./// - /// A SSH error where is the message from the remote host. + /// Permission to perform the operation was denied by the remote host. -or- An SSH command was denied by the server. + /// was not found on the remote host. + /// An SSH error where is the message from the remote host. /// The method was called after the client was disposed. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// void DownloadFile(string path, Stream output, Action? downloadCallback = null); /// - /// Asynchronously downloads remote file specified by the path into the stream. + /// Asynchronously downloads a remote file into a . /// - /// File to download. - /// Stream to write the file into. + /// The path to the remote file. + /// The to write the file into. /// The to observe. /// A that represents the asynchronous download operation. - /// is . - /// is or contains only whitespace characters. + /// or is . + /// is empty 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. - /// was not found on the remote host./// - /// A SSH error where is the message from the remote host. + /// Permission to perform the operation was denied by the remote host. -or- An SSH command was denied by the server. + /// was not found on the remote host. + /// An SSH error where is the message from the remote host. /// The method was called after the client was disposed. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default); /// @@ -1089,52 +1083,46 @@ public interface ISftpClient : IBaseClient IEnumerable SynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern); /// - /// Uploads stream into remote file. + /// Uploads a to a remote file path. /// - /// Data input stream. - /// Remote file path. + /// The to write to the remote path. + /// The remote file path to write to. /// The upload callback. - /// is . - /// is or contains only whitespace characters. + /// or is . + /// is empty or contains only whitespace characters. /// Client is not connected. - /// Permission to upload the file 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. + /// Permission to upload the file was denied by the remote host. -or- An SSH command was denied by the server. + /// An SSH error where is the message from the remote host. /// The method was called after the client was disposed. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// void UploadFile(Stream input, string path, Action? uploadCallback = null); /// - /// Uploads stream into remote file. + /// Uploads a to a remote file path. /// - /// Data input stream. - /// Remote file path. - /// if set to then existing file will be overwritten. + /// The to write to the remote path. + /// The remote file path to write to. + /// Whether the remote file can be overwritten if it already exists. /// The upload callback. - /// is . - /// is or contains only whitespace characters. + /// or is . + /// is empty or contains only whitespace characters. /// Client is not connected. - /// Permission to upload the file 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. + /// Permission to upload the file was denied by the remote host. -or- An SSH command was denied by the server. + /// An SSH error where is the message from the remote host. /// The method was called after the client was disposed. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// void UploadFile(Stream input, string path, bool canOverride, Action? uploadCallback = null); /// - /// Asynchronously uploads stream into remote file. + /// Asynchronously uploads a to a remote file path. /// - /// Data input stream. - /// Remote file path. + /// The to write to the remote path. + /// The remote file path to write to. /// The to observe. /// A that represents the asynchronous upload operation. - /// is . - /// is or contains only whitespace characters. + /// or is . + /// is empty or contains only whitespace characters. /// Client is not connected. - /// Permission to upload the file 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. + /// Permission to upload the file was denied by the remote host. -or- An SSH command was denied by the server. + /// An SSH error where is the message from the remote host. /// The method was called after the client was disposed. Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default); diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index dcb209515..50a2e9cbd 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -893,22 +893,7 @@ public async Task ExistsAsync(string path, CancellationToken cancellationT } } - /// - /// Downloads remote file specified by the path into the stream. - /// - /// File to download. - /// Stream to write the file into. - /// The download callback. - /// is . - /// 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. - /// was not found on the remote host./// - /// A SSH error where is the message from the remote host. - /// The method was called after the client was disposed. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// + /// public void DownloadFile(string path, Stream output, Action? downloadCallback = null) { CheckDisposed(); @@ -1031,42 +1016,13 @@ public void EndDownloadFile(IAsyncResult asyncResult) ar.EndInvoke(); } - /// - /// Uploads stream into remote file. - /// - /// Data input stream. - /// Remote file path. - /// The upload callback. - /// is . - /// is or contains only whitespace characters. - /// Client is not connected. - /// Permission to upload the file 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. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// + /// public void UploadFile(Stream input, string path, Action? uploadCallback = null) { UploadFile(input, path, canOverride: true, uploadCallback); } - /// - /// Uploads stream into remote file. - /// - /// Data input stream. - /// Remote file path. - /// if set to then existing file will be overwritten. - /// The upload callback. - /// is . - /// is or contains only whitespace characters. - /// Client is not connected. - /// Permission to upload the file 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. - /// - /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. - /// + /// public void UploadFile(Stream input, string path, bool canOverride, Action? uploadCallback = null) { CheckDisposed(); @@ -1090,7 +1046,7 @@ public Task UploadFileAsync(Stream input, string path, CancellationToken cancell { CheckDisposed(); - return InternalUploadFileAsync(input, path, FileMode.Create, cancellationToken); + return InternalUploadFileAsync(input, path, cancellationToken); } /// @@ -2552,7 +2508,7 @@ private void InternalUploadFile(Stream input, string path, Flags flags, SftpUplo responseReceivedWaitHandle.Dispose(); } - private async Task InternalUploadFileAsync(Stream input, string path, FileMode fileMode, CancellationToken cancellationToken) + private async Task InternalUploadFileAsync(Stream input, string path, CancellationToken cancellationToken) { ThrowHelper.ThrowIfNull(input); ThrowHelper.ThrowIfNullOrWhiteSpace(path); @@ -2565,7 +2521,7 @@ private async Task InternalUploadFileAsync(Stream input, string path, FileMode f cancellationToken.ThrowIfCancellationRequested(); var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); - var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, fileMode, FileAccess.Write, (int)_bufferSize, cancellationToken); + var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, FileMode.Create, FileAccess.Write, (int)_bufferSize, cancellationToken); using (var output = await openStreamTask.ConfigureAwait(false)) {