ソースを参照

Add UploadFileAsync and DownloadFileAsync methods (#1634)

* add interface methods

* add internal file methods

* impl interface methods

* tweak buffer size usage

* swap tests with async upload

* swap more upload file references

* add async download tests

* add upload/download integration async test

* check if net48

* silence not await warning

* try tweaking test init

* remove request close from upload

* dispose already closes, remove dup call

* remove extra upload overload

* inherit doc

* configure await

* remove excess util functions

* add cancel throws

* add cancellation tests

* missed one configure await

* use default buffer size

* private ctor

* docs

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Noah Dela Rosa 5 ヶ月 前
コミット
d08c4aaa8a

+ 54 - 32
src/Renci.SshNet/ISftpClient.cs

@@ -556,23 +556,36 @@ namespace Renci.SshNet
         Task DeleteFileAsync(string path, CancellationToken cancellationToken);
 
         /// <summary>
-        /// Downloads remote file specified by the path into the stream.
+        /// Downloads a remote file into a <see cref="Stream"/>.
         /// </summary>
-        /// <param name="path">File to download.</param>
-        /// <param name="output">Stream to write the file into.</param>
+        /// <param name="path">The path to the remote file.</param>
+        /// <param name="output">The <see cref="Stream"/> to write the file into.</param>
         /// <param name="downloadCallback">The download callback.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
-        /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="output"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentException"><paramref name="path"/> is empty 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="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>///
-        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> An SSH command was denied by the server.</exception>
+        /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
+        /// <exception cref="SshException">An 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>
-        /// <remarks>
-        /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
-        /// </remarks>
         void DownloadFile(string path, Stream output, Action<ulong>? downloadCallback = null);
 
+        /// <summary>
+        /// Asynchronously downloads a remote file into a <see cref="Stream"/>.
+        /// </summary>
+        /// <param name="path">The path to the remote file.</param>
+        /// <param name="output">The <see cref="Stream"/> to write the file into.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous download operation.</returns>
+        /// <exception cref="ArgumentNullException"><paramref name="output"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentException"><paramref name="path"/> is empty 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> An SSH command was denied by the server.</exception>
+        /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
+        /// <exception cref="SshException">An 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>
+        Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default);
+
         /// <summary>
         /// Ends an asynchronous file downloading into the stream.
         /// </summary>
@@ -1070,40 +1083,49 @@ namespace Renci.SshNet
         IEnumerable<FileInfo> SynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern);
 
         /// <summary>
-        /// Uploads stream into remote file.
+        /// Uploads a <see cref="Stream"/> to a remote file path.
         /// </summary>
-        /// <param name="input">Data input stream.</param>
-        /// <param name="path">Remote file path.</param>
+        /// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
+        /// <param name="path">The remote file path to write to.</param>
         /// <param name="uploadCallback">The upload callback.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
-        /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="input" /> or <paramref name="path" /> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
-        /// <exception cref="SftpPermissionDeniedException">Permission to upload the file 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="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> An SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">An 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>
-        /// <remarks>
-        /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
-        /// </remarks>
         void UploadFile(Stream input, string path, Action<ulong>? uploadCallback = null);
 
         /// <summary>
-        /// Uploads stream into remote file.
+        /// Uploads a <see cref="Stream"/> to a remote file path.
         /// </summary>
-        /// <param name="input">Data input stream.</param>
-        /// <param name="path">Remote file path.</param>
-        /// <param name="canOverride">if set to <see langword="true"/> then existing file will be overwritten.</param>
+        /// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
+        /// <param name="path">The remote file path to write to.</param>
+        /// <param name="canOverride">Whether the remote file can be overwritten if it already exists.</param>
         /// <param name="uploadCallback">The upload callback.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
-        /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="input" /> or <paramref name="path" /> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
-        /// <exception cref="SftpPermissionDeniedException">Permission to upload the file 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="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> An SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">An 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>
-        /// <remarks>
-        /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
-        /// </remarks>
         void UploadFile(Stream input, string path, bool canOverride, Action<ulong>? uploadCallback = null);
 
+        /// <summary>
+        /// Asynchronously uploads a <see cref="Stream"/> to a remote file path.
+        /// </summary>
+        /// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
+        /// <param name="path">The remote file path to write to.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous upload operation.</returns>
+        /// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> An SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">An 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>
+        Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default);
+
         /// <summary>
         /// Writes the specified byte array to the specified file, and closes the file.
         /// </summary>

+ 61 - 47
src/Renci.SshNet/SftpClient.cs

@@ -893,22 +893,7 @@ namespace Renci.SshNet
             }
         }
 
-        /// <summary>
-        /// Downloads remote file specified by the path into the stream.
-        /// </summary>
-        /// <param name="path">File to download.</param>
-        /// <param name="output">Stream to write the file into.</param>
-        /// <param name="downloadCallback">The download callback.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
-        /// <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="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</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>
-        /// <remarks>
-        /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
-        /// </remarks>
+        /// <inheritdoc />
         public void DownloadFile(string path, Stream output, Action<ulong>? downloadCallback = null)
         {
             CheckDisposed();
@@ -916,6 +901,14 @@ namespace Renci.SshNet
             InternalDownloadFile(path, output, asyncResult: null, downloadCallback);
         }
 
+        /// <inheritdoc />
+        public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default)
+        {
+            CheckDisposed();
+
+            return InternalDownloadFileAsync(path, output, cancellationToken);
+        }
+
         /// <summary>
         /// Begins an asynchronous file downloading into the stream.
         /// </summary>
@@ -1023,42 +1016,13 @@ namespace Renci.SshNet
             ar.EndInvoke();
         }
 
-        /// <summary>
-        /// Uploads stream into remote file.
-        /// </summary>
-        /// <param name="input">Data input stream.</param>
-        /// <param name="path">Remote file path.</param>
-        /// <param name="uploadCallback">The upload callback.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
-        /// <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 upload the file 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>
-        /// <remarks>
-        /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
-        /// </remarks>
+        /// <inheritdoc/>
         public void UploadFile(Stream input, string path, Action<ulong>? uploadCallback = null)
         {
             UploadFile(input, path, canOverride: true, uploadCallback);
         }
 
-        /// <summary>
-        /// Uploads stream into remote file.
-        /// </summary>
-        /// <param name="input">Data input stream.</param>
-        /// <param name="path">Remote file path.</param>
-        /// <param name="canOverride">if set to <see langword="true"/> then existing file will be overwritten.</param>
-        /// <param name="uploadCallback">The upload callback.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
-        /// <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 upload the file 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>
-        /// <remarks>
-        /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
-        /// </remarks>
+        /// <inheritdoc/>
         public void UploadFile(Stream input, string path, bool canOverride, Action<ulong>? uploadCallback = null)
         {
             CheckDisposed();
@@ -1077,6 +1041,14 @@ namespace Renci.SshNet
             InternalUploadFile(input, path, flags, asyncResult: null, uploadCallback);
         }
 
+        /// <inheritdoc />
+        public Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default)
+        {
+            CheckDisposed();
+
+            return InternalUploadFileAsync(input, path, cancellationToken);
+        }
+
         /// <summary>
         /// Begins an asynchronous uploading the stream into remote file.
         /// </summary>
@@ -2433,6 +2405,27 @@ namespace Renci.SshNet
             }
         }
 
+        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.");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+            var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, FileMode.Open, FileAccess.Read, (int)_bufferSize, cancellationToken);
+
+            using (var input = await openStreamTask.ConfigureAwait(false))
+            {
+                await input.CopyToAsync(output, 81920, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
         /// <summary>
         /// Internals the upload file.
         /// </summary>
@@ -2515,6 +2508,27 @@ namespace Renci.SshNet
             responseReceivedWaitHandle.Dispose();
         }
 
+        private async Task InternalUploadFileAsync(Stream input, string path, CancellationToken cancellationToken)
+        {
+            ThrowHelper.ThrowIfNull(input);
+            ThrowHelper.ThrowIfNullOrWhiteSpace(path);
+
+            if (_sftpSession is null)
+            {
+                throw new SshConnectionException("Client not connected.");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+            var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, FileMode.Create, FileAccess.Write, (int)_bufferSize, cancellationToken);
+
+            using (var output = await openStreamTask.ConfigureAwait(false))
+            {
+                await input.CopyToAsync(output, 81920, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
         /// <summary>
         /// Called when client is connected to the server.
         /// </summary>

+ 38 - 0
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs

@@ -31,6 +31,44 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             }
         }
 
+        [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<SftpPermissionDeniedException>(() => 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<SftpPathNotFoundException>(() => sftp.DownloadFileAsync("/xxx/eee/yyy", Stream.Null));
+            }
+        }
+
+        [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<OperationCanceledException>(() => sftp.DownloadFileAsync("/xxx/eee/yyy", Stream.Null, cancelledToken));
+            }
+        }
+
         [TestMethod]
         [TestCategory("Sftp")]
         [Description("Test passing null to BeginDownloadFile")]

+ 65 - 0
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs

@@ -51,6 +51,49 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             }
         }
 
+        [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();
+
+                CreateTestFile(uploadedFileName, 1);
+
+                //  Calculate has value
+                var uploadedHash = CalculateMD5(uploadedFileName);
+
+                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 = CalculateMD5(downloadedFileName);
+
+                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()
@@ -73,6 +116,28 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
             }
         }
 
+        [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<OperationCanceledException>(() => sftp.UploadFileAsync(file, remoteFileName, cancelledToken));
+                }
+            }
+        }
+
         [TestMethod]
         [TestCategory("Sftp")]
         public void Test_Sftp_Multiple_Async_Upload_And_Download_10Files_5MB_Each()

+ 7 - 7
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs

@@ -88,7 +88,7 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
         {
             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 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
         {
             using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
-                sftp.Connect();
+                await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
 
                 await Assert.ThrowsExceptionAsync<SftpPathNotFoundException>(() => sftp.GetAsync("/xyz", default));
             }
@@ -115,9 +115,9 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
         {
             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 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
         {
             using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
             {
-                sftp.Connect();
+                await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
 
                 await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => sftp.GetAsync(null, default));
             }
@@ -145,9 +145,9 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
         {
             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);
 

+ 19 - 13
test/Renci.SshNet.IntegrationTests/SftpClientTests.cs

@@ -6,14 +6,26 @@ namespace Renci.SshNet.IntegrationTests
     /// The SFTP client integration tests
     /// </summary>
     [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]
@@ -65,7 +77,7 @@ namespace Renci.SshNet.IntegrationTests
 
             // 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).ConfigureAwait(false);
             Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath));
 
             // Check if ListDirectory works
@@ -118,12 +130,12 @@ namespace Renci.SshNet.IntegrationTests
             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));
-            _sftpClient.UploadFile(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);
@@ -142,7 +154,7 @@ namespace Renci.SshNet.IntegrationTests
             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);
@@ -158,18 +170,12 @@ namespace Renci.SshNet.IntegrationTests
 
             // 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).ConfigureAwait(false);
             Assert.IsTrue(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false));
 
             await _sftpClient.DeleteAsync(testFileName, CancellationToken.None).ConfigureAwait(false);
 
             Assert.IsFalse(await _sftpClient.ExistsAsync(testFileName).ConfigureAwait(false));
         }
-
-        public void Dispose()
-        {
-            _sftpClient.Disconnect();
-            _sftpClient.Dispose();
-        }
     }
 }

+ 1 - 1
test/Renci.SshNet.IntegrationTests/SftpTests.cs

@@ -4115,7 +4115,7 @@ namespace Renci.SshNet.IntegrationTests
                     {
                         uploadStream.Position = 0;
 
-                        client.UploadFile(uploadStream, "gert.txt");
+                        await client.UploadFileAsync(uploadStream, "gert.txt").ConfigureAwait(false);
 
                         uploadStream.Position = 0;