Skip to content

Add UploadFileAsync and DownloadFileAsync methods #1634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from

Conversation

sdt-ndelarosa
Copy link

@sdt-ndelarosa sdt-ndelarosa commented Apr 24, 2025

Adds TAP methods for UploadFile and DownloadFile to ISftpClient.

Other Solutions

While there are other options to define both the UploadFileAsync and DownloadFileAsync methods, such as the extension methods approach and the previously proposed pull request, #1515 , this pull request has the following benefits over the other solutions:

  • First class async/await support for uploading and downloading
    • Users no longer have to rely on implementing their own extension methods
  • Cleaner interop with the sync versions of the UploadFile and DownloadFile methods
    • Method signatures have a closer match
  • Upload and downloads now work with relative paths instead of just absolute paths
  • Deeper async/await call chain, as compared to BeginUploadFile, which pretty much runs the sync UploadFile method, but just in a different thread

@sdt-ndelarosa sdt-ndelarosa marked this pull request as ready for review April 24, 2025 16:59
@sdt-ndelarosa
Copy link
Author

actions don't seem to be triggering, gonna try closing and reopening

@Rob-Hague
Copy link
Collaborator

Hi, I will make time to review most likely at some point next week. FYI for first time contributors the CI needs to be run manually (some kind of github security measure)

Copy link
Collaborator

@Rob-Hague Rob-Hague left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I think it could do with testing cancellation - i.e. that it throws OperationCanceledException when the token is cancelled.

It would probably not be very reliable to test cancelling during a file download (at least not in an integration test), but we could at least have a test for when a cancelled token is supplied

@@ -191,7 +191,7 @@ public TimeSpan Timeout
}
}

private SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use SftpFileStream.OpenAsync (line 331) instead of this constructor?

Copy link
Author

@sdt-ndelarosa sdt-ndelarosa May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm sticking with using the ctor directly instead of using the OpenAsync method mainly because calculating the optimal write length needs the direct file handle, which is used for the copy buffer size

i might be misusing the intended use case for the CalculateOptimalWriteLength method, and if so i can just rely of the default buffer size of 81920 and swap out the call to OpenAsync if need be

@sdt-ndelarosa sdt-ndelarosa requested a review from Rob-Hague May 2, 2025 15:57
Comment on lines +2570 to +2577
var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
var handle = await _sftpSession.RequestOpenAsync(fullPath, Flags.Write | flags, cancellationToken).ConfigureAwait(false);

using (var output = new SftpFileStream(_sftpSession, fullPath, FileAccess.Write, (int)_bufferSize, handle, 0L))
{
var bufferSize = (int)_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle);
await input.CopyToAsync(output, bufferSize, cancellationToken).ConfigureAwait(false);
}
Copy link
Collaborator

@Rob-Hague Rob-Hague May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(moving the conversation here)

i might be misusing the intended use case for the CalculateOptimalWriteLength method, and if so i can just rely of the default buffer size of 81920 and swap out the call to OpenAsync if need be

I think I would prefer that. I have doubts that CalculateOptimalWriteLength lives up to its name, so if we can reuse existing code and simplify this bit then that sounds good.

It does raise an interesting point that here you are canonicalizing the path while SftpFileStream currently does not. I've just spent far too long looking at it but I think it would be fine to add to SftpFileStream. I think it should make this (failing) test pass:

diff --git a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
index 67b3b28c..e90cf704 100644
--- a/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
+++ b/test/Renci.SshNet.IntegrationTests/SftpClientTests.cs
@@ -162,6 +162,46 @@ public async Task Create_directory_and_delete_it_using_DeleteAsync()
             Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
         }
 
+        [TestMethod]
+        public async Task Open_PathCanonicalized()
+        {
+            var testDirectory = "/home/sshnet/sshnet-test";
+
+            // Create new directory and check if it exists
+            await _sftpClient.CreateDirectoryAsync(testDirectory).ConfigureAwait(false);
+
+            await _sftpClient.ChangeDirectoryAsync(testDirectory).ConfigureAwait(false);
+
+            Assert.AreEqual(testDirectory, _sftpClient.WorkingDirectory);
+
+            Assert.IsFalse(await _sftpClient.ExistsAsync("/home/sshnet/tempasync.txt").ConfigureAwait(false));
+            Assert.IsFalse(await _sftpClient.ExistsAsync("/home/sshnet/tempsync.txt").ConfigureAwait(false));
+
+            using (var stream = await _sftpClient.OpenAsync("../tempasync.txt", FileMode.Create, FileAccess.ReadWrite, default).ConfigureAwait(false))
+            {
+                await stream.WriteAsync(Encoding.ASCII.GetBytes("test"), 0, 4).ConfigureAwait(false);
+            }
+
+            using (var stream = _sftpClient.Open("../tempsync.txt", FileMode.Create, FileAccess.ReadWrite))
+            {
+                await stream.WriteAsync(Encoding.ASCII.GetBytes("test"), 0, 4).ConfigureAwait(false);
+            }
+
+            Assert.IsTrue(await _sftpClient.ExistsAsync("/home/sshnet/tempasync.txt").ConfigureAwait(false));
+            Assert.IsTrue(await _sftpClient.ExistsAsync("/home/sshnet/tempsync.txt").ConfigureAwait(false));
+
+            await _sftpClient.DeleteAsync("../tempasync.txt", CancellationToken.None).ConfigureAwait(false);
+            await _sftpClient.DeleteAsync("../tempsync.txt", CancellationToken.None).ConfigureAwait(false);
+
+            await _sftpClient.ChangeDirectoryAsync("../").ConfigureAwait(false);
+
+            await _sftpClient.DeleteAsync("sshnet-test", CancellationToken.None).ConfigureAwait(false);
+
+            Assert.IsFalse(await _sftpClient.ExistsAsync(testDirectory).ConfigureAwait(false));
+            Assert.IsFalse(await _sftpClient.ExistsAsync("/home/sshnet/tempasync.txt").ConfigureAwait(false));
+            Assert.IsFalse(await _sftpClient.ExistsAsync("/home/sshnet/tempsync.txt").ConfigureAwait(false));
+        }
+
         [TestMethod]
         public async Task Create_file_and_delete_using_DeleteAsync()
         {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canonicalizing the path in SftpFileStream will probably cause failures in some painfully strict unit tests, in which case it can be left for later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants