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

Merged
merged 24 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 54 additions & 32 deletions src/Renci.SshNet/ISftpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -556,23 +556,36 @@ public interface ISftpClient : IBaseClient
Task DeleteFileAsync(string path, CancellationToken cancellationToken);

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

/// <summary>
/// Asynchronously downloads a remote file into a <see cref="Stream"/>.
/// </summary>
/// <param name="path">The path to the remote file.</param>
/// <param name="output">The <see cref="Stream"/> to write the file into.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous download operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="output"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is empty or contains only whitespace characters.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <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>
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default);

/// <summary>
/// Ends an asynchronous file downloading into the stream.
/// </summary>
Expand Down Expand Up @@ -1070,40 +1083,49 @@ public interface ISftpClient : IBaseClient
IEnumerable<FileInfo> SynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern);

/// <summary>
/// Uploads stream into remote file.
/// Uploads a <see cref="Stream"/> to a remote file path.
/// </summary>
/// <param name="input">Data input stream.</param>
/// <param name="path">Remote file path.</param>
/// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
/// <param name="path">The remote file path to write to.</param>
/// <param name="uploadCallback">The upload callback.</param>
/// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="input" /> or <paramref name="path" /> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <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>
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
/// <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>
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
/// <remarks>
/// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
/// </remarks>
void UploadFile(Stream input, string path, Action<ulong>? uploadCallback = null);

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

/// <summary>
/// Asynchronously uploads a <see cref="Stream"/> to a remote file path.
/// </summary>
/// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
/// <param name="path">The remote file path to write to.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous upload operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <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>
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default);

/// <summary>
/// Writes the specified byte array to the specified file, and closes the file.
/// </summary>
Expand Down
108 changes: 61 additions & 47 deletions src/Renci.SshNet/SftpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -893,29 +893,22 @@ public async Task<bool> ExistsAsync(string path, CancellationToken cancellationT
}
}

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

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

/// <inheritdoc />
public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default)
{
CheckDisposed();

return InternalDownloadFileAsync(path, output, cancellationToken);
}

/// <summary>
/// Begins an asynchronous file downloading into the stream.
/// </summary>
Expand Down Expand Up @@ -1023,42 +1016,13 @@ public void EndDownloadFile(IAsyncResult asyncResult)
ar.EndInvoke();
}

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

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

/// <inheritdoc />
public Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default)
{
CheckDisposed();

return InternalUploadFileAsync(input, path, cancellationToken);
}

/// <summary>
/// Begins an asynchronous uploading the stream into remote file.
/// </summary>
Expand Down Expand Up @@ -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);
}
}

/// <summary>
/// Internals the upload file.
/// </summary>
Expand Down Expand Up @@ -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);
}
}

/// <summary>
/// Called when client is connected to the server.
/// </summary>
Expand Down
Loading
Loading