diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index 2c37bbcbe..04525dcbc 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -556,23 +556,36 @@ 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 a remote file into a . + /// + /// The path to the remote file. + /// The to write the file into. + /// The to observe. + /// A that represents the asynchronous download operation. + /// 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- 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. + Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default); + /// /// Ends an asynchronous file downloading into the stream. /// @@ -1070,40 +1083,49 @@ 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 a to a 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. + /// 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- 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); + /// /// 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 7cf5e62a3..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(); @@ -916,6 +901,14 @@ public void DownloadFile(string path, Stream output, Action? downloadCall InternalDownloadFile(path, output, asyncResult: null, downloadCallback); } + /// + public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default) + { + CheckDisposed(); + + return InternalDownloadFileAsync(path, output, cancellationToken); + } + /// /// Begins an asynchronous file downloading into the stream. /// @@ -1023,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(); @@ -1077,6 +1041,14 @@ public void UploadFile(Stream input, string path, bool canOverride, Action + public Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default) + { + CheckDisposed(); + + return InternalUploadFileAsync(input, path, cancellationToken); + } + /// /// Begins an asynchronous uploading the stream into remote file. /// @@ -2433,6 +2405,27 @@ 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."); + } + + 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); + } + } + /// /// Internals the upload file. /// @@ -2515,6 +2508,27 @@ private void InternalUploadFile(Stream input, string path, Flags flags, SftpUplo 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); + } + } + /// /// Called when client is connected to the server. /// diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs index d1475c5f3..1b87ecfbd 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs @@ -31,6 +31,44 @@ 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")] + 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 e5cd91400..a3c626fa6 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(); + + 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 @@ 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() 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); diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs index 52c4223f4..67b3b28c1 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] @@ -65,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)); - _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 @@ 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)); - _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 @@ 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); @@ -158,18 +170,12 @@ 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).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(); - } } } diff --git a/test/Renci.SshNet.IntegrationTests/SftpTests.cs b/test/Renci.SshNet.IntegrationTests/SftpTests.cs index 8a3c229f1..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; - client.UploadFile(uploadStream, "gert.txt"); + await client.UploadFileAsync(uploadStream, "gert.txt").ConfigureAwait(false); uploadStream.Position = 0;