diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs index 2c37bbcb..04525dcb 100644 --- a/src/Renci.SshNet/ISftpClient.cs +++ b/src/Renci.SshNet/ISftpClient.cs @@ -556,23 +556,36 @@ namespace Renci.SshNet 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 @@ namespace Renci.SshNet 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 7cf5e62a..50a2e9cb 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -893,22 +893,7 @@ namespace Renci.SshNet } } - /// - /// 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 @@ namespace Renci.SshNet 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 @@ namespace Renci.SshNet 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 @@ namespace Renci.SshNet InternalUploadFile(input, path, flags, asyncResult: null, uploadCallback); } + /// + 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 @@ 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); + } + } + /// /// Internals the upload file. /// @@ -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); + } + } + /// /// 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 d1475c5f..1b87ecfb 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs +++ b/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(() => 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 e5cd9140..a3c626fa 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs +++ b/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(() => 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 34819a6e..344819b3 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs +++ b/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(() => 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(() => 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); diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs index 52c4223f..67b3b28c 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 @@ 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(); - } } } diff --git a/test/Renci.SshNet.IntegrationTests/SftpTests.cs b/test/Renci.SshNet.IntegrationTests/SftpTests.cs index 8a3c229f..83275e0e 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpTests.cs +++ b/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;