Skip to content

Commit d08c4aa

Browse files
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>
1 parent 7c07b10 commit d08c4aa

File tree

7 files changed

+245
-100
lines changed

7 files changed

+245
-100
lines changed

src/Renci.SshNet/ISftpClient.cs

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -556,23 +556,36 @@ public interface ISftpClient : IBaseClient
556556
Task DeleteFileAsync(string path, CancellationToken cancellationToken);
557557

558558
/// <summary>
559-
/// Downloads remote file specified by the path into the stream.
559+
/// Downloads a remote file into a <see cref="Stream"/>.
560560
/// </summary>
561-
/// <param name="path">File to download.</param>
562-
/// <param name="output">Stream to write the file into.</param>
561+
/// <param name="path">The path to the remote file.</param>
562+
/// <param name="output">The <see cref="Stream"/> to write the file into.</param>
563563
/// <param name="downloadCallback">The download callback.</param>
564-
/// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
565-
/// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
564+
/// <exception cref="ArgumentNullException"><paramref name="output"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
565+
/// <exception cref="ArgumentException"><paramref name="path"/> is empty or contains only whitespace characters.</exception>
566566
/// <exception cref="SshConnectionException">Client is not connected.</exception>
567-
/// <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>
568-
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>///
569-
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
567+
/// <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>
568+
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
569+
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
570570
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
571-
/// <remarks>
572-
/// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
573-
/// </remarks>
574571
void DownloadFile(string path, Stream output, Action<ulong>? downloadCallback = null);
575572

573+
/// <summary>
574+
/// Asynchronously downloads a remote file into a <see cref="Stream"/>.
575+
/// </summary>
576+
/// <param name="path">The path to the remote file.</param>
577+
/// <param name="output">The <see cref="Stream"/> to write the file into.</param>
578+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
579+
/// <returns>A <see cref="Task"/> that represents the asynchronous download operation.</returns>
580+
/// <exception cref="ArgumentNullException"><paramref name="output"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
581+
/// <exception cref="ArgumentException"><paramref name="path"/> is empty or contains only whitespace characters.</exception>
582+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
583+
/// <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>
584+
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
585+
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
586+
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
587+
Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default);
588+
576589
/// <summary>
577590
/// Ends an asynchronous file downloading into the stream.
578591
/// </summary>
@@ -1070,40 +1083,49 @@ public interface ISftpClient : IBaseClient
10701083
IEnumerable<FileInfo> SynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern);
10711084

10721085
/// <summary>
1073-
/// Uploads stream into remote file.
1086+
/// Uploads a <see cref="Stream"/> to a remote file path.
10741087
/// </summary>
1075-
/// <param name="input">Data input stream.</param>
1076-
/// <param name="path">Remote file path.</param>
1088+
/// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
1089+
/// <param name="path">The remote file path to write to.</param>
10771090
/// <param name="uploadCallback">The upload callback.</param>
1078-
/// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
1079-
/// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
1091+
/// <exception cref="ArgumentNullException"><paramref name="input" /> or <paramref name="path" /> is <see langword="null"/>.</exception>
1092+
/// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
10801093
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1081-
/// <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>
1082-
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
1094+
/// <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>
1095+
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
10831096
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1084-
/// <remarks>
1085-
/// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
1086-
/// </remarks>
10871097
void UploadFile(Stream input, string path, Action<ulong>? uploadCallback = null);
10881098

10891099
/// <summary>
1090-
/// Uploads stream into remote file.
1100+
/// Uploads a <see cref="Stream"/> to a remote file path.
10911101
/// </summary>
1092-
/// <param name="input">Data input stream.</param>
1093-
/// <param name="path">Remote file path.</param>
1094-
/// <param name="canOverride">if set to <see langword="true"/> then existing file will be overwritten.</param>
1102+
/// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
1103+
/// <param name="path">The remote file path to write to.</param>
1104+
/// <param name="canOverride">Whether the remote file can be overwritten if it already exists.</param>
10951105
/// <param name="uploadCallback">The upload callback.</param>
1096-
/// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
1097-
/// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
1106+
/// <exception cref="ArgumentNullException"><paramref name="input" /> or <paramref name="path" /> is <see langword="null"/>.</exception>
1107+
/// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
10981108
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1099-
/// <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>
1100-
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
1109+
/// <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>
1110+
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
11011111
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1102-
/// <remarks>
1103-
/// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
1104-
/// </remarks>
11051112
void UploadFile(Stream input, string path, bool canOverride, Action<ulong>? uploadCallback = null);
11061113

1114+
/// <summary>
1115+
/// Asynchronously uploads a <see cref="Stream"/> to a remote file path.
1116+
/// </summary>
1117+
/// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
1118+
/// <param name="path">The remote file path to write to.</param>
1119+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
1120+
/// <returns>A <see cref="Task"/> that represents the asynchronous upload operation.</returns>
1121+
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
1122+
/// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
1123+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1124+
/// <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>
1125+
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
1126+
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1127+
Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default);
1128+
11071129
/// <summary>
11081130
/// Writes the specified byte array to the specified file, and closes the file.
11091131
/// </summary>

src/Renci.SshNet/SftpClient.cs

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -893,29 +893,22 @@ public async Task<bool> ExistsAsync(string path, CancellationToken cancellationT
893893
}
894894
}
895895

896-
/// <summary>
897-
/// Downloads remote file specified by the path into the stream.
898-
/// </summary>
899-
/// <param name="path">File to download.</param>
900-
/// <param name="output">Stream to write the file into.</param>
901-
/// <param name="downloadCallback">The download callback.</param>
902-
/// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
903-
/// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
904-
/// <exception cref="SshConnectionException">Client is not connected.</exception>
905-
/// <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>
906-
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>///
907-
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
908-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
909-
/// <remarks>
910-
/// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
911-
/// </remarks>
896+
/// <inheritdoc />
912897
public void DownloadFile(string path, Stream output, Action<ulong>? downloadCallback = null)
913898
{
914899
CheckDisposed();
915900

916901
InternalDownloadFile(path, output, asyncResult: null, downloadCallback);
917902
}
918903

904+
/// <inheritdoc />
905+
public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default)
906+
{
907+
CheckDisposed();
908+
909+
return InternalDownloadFileAsync(path, output, cancellationToken);
910+
}
911+
919912
/// <summary>
920913
/// Begins an asynchronous file downloading into the stream.
921914
/// </summary>
@@ -1023,42 +1016,13 @@ public void EndDownloadFile(IAsyncResult asyncResult)
10231016
ar.EndInvoke();
10241017
}
10251018

1026-
/// <summary>
1027-
/// Uploads stream into remote file.
1028-
/// </summary>
1029-
/// <param name="input">Data input stream.</param>
1030-
/// <param name="path">Remote file path.</param>
1031-
/// <param name="uploadCallback">The upload callback.</param>
1032-
/// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
1033-
/// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
1034-
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1035-
/// <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>
1036-
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
1037-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1038-
/// <remarks>
1039-
/// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
1040-
/// </remarks>
1019+
/// <inheritdoc/>
10411020
public void UploadFile(Stream input, string path, Action<ulong>? uploadCallback = null)
10421021
{
10431022
UploadFile(input, path, canOverride: true, uploadCallback);
10441023
}
10451024

1046-
/// <summary>
1047-
/// Uploads stream into remote file.
1048-
/// </summary>
1049-
/// <param name="input">Data input stream.</param>
1050-
/// <param name="path">Remote file path.</param>
1051-
/// <param name="canOverride">if set to <see langword="true"/> then existing file will be overwritten.</param>
1052-
/// <param name="uploadCallback">The upload callback.</param>
1053-
/// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
1054-
/// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
1055-
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1056-
/// <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>
1057-
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
1058-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1059-
/// <remarks>
1060-
/// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
1061-
/// </remarks>
1025+
/// <inheritdoc/>
10621026
public void UploadFile(Stream input, string path, bool canOverride, Action<ulong>? uploadCallback = null)
10631027
{
10641028
CheckDisposed();
@@ -1077,6 +1041,14 @@ public void UploadFile(Stream input, string path, bool canOverride, Action<ulong
10771041
InternalUploadFile(input, path, flags, asyncResult: null, uploadCallback);
10781042
}
10791043

1044+
/// <inheritdoc />
1045+
public Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default)
1046+
{
1047+
CheckDisposed();
1048+
1049+
return InternalUploadFileAsync(input, path, cancellationToken);
1050+
}
1051+
10801052
/// <summary>
10811053
/// Begins an asynchronous uploading the stream into remote file.
10821054
/// </summary>
@@ -2433,6 +2405,27 @@ private void InternalDownloadFile(string path, Stream output, SftpDownloadAsyncR
24332405
}
24342406
}
24352407

2408+
private async Task InternalDownloadFileAsync(string path, Stream output, CancellationToken cancellationToken)
2409+
{
2410+
ThrowHelper.ThrowIfNull(output);
2411+
ThrowHelper.ThrowIfNullOrWhiteSpace(path);
2412+
2413+
if (_sftpSession is null)
2414+
{
2415+
throw new SshConnectionException("Client not connected.");
2416+
}
2417+
2418+
cancellationToken.ThrowIfCancellationRequested();
2419+
2420+
var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
2421+
var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, FileMode.Open, FileAccess.Read, (int)_bufferSize, cancellationToken);
2422+
2423+
using (var input = await openStreamTask.ConfigureAwait(false))
2424+
{
2425+
await input.CopyToAsync(output, 81920, cancellationToken).ConfigureAwait(false);
2426+
}
2427+
}
2428+
24362429
/// <summary>
24372430
/// Internals the upload file.
24382431
/// </summary>
@@ -2515,6 +2508,27 @@ private void InternalUploadFile(Stream input, string path, Flags flags, SftpUplo
25152508
responseReceivedWaitHandle.Dispose();
25162509
}
25172510

2511+
private async Task InternalUploadFileAsync(Stream input, string path, CancellationToken cancellationToken)
2512+
{
2513+
ThrowHelper.ThrowIfNull(input);
2514+
ThrowHelper.ThrowIfNullOrWhiteSpace(path);
2515+
2516+
if (_sftpSession is null)
2517+
{
2518+
throw new SshConnectionException("Client not connected.");
2519+
}
2520+
2521+
cancellationToken.ThrowIfCancellationRequested();
2522+
2523+
var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
2524+
var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, FileMode.Create, FileAccess.Write, (int)_bufferSize, cancellationToken);
2525+
2526+
using (var output = await openStreamTask.ConfigureAwait(false))
2527+
{
2528+
await input.CopyToAsync(output, 81920, cancellationToken).ConfigureAwait(false);
2529+
}
2530+
}
2531+
25182532
/// <summary>
25192533
/// Called when client is connected to the server.
25202534
/// </summary>

0 commit comments

Comments
 (0)