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;