Skip to content

Commit ad73c46

Browse files
committed
Change to IProgress and also add to UploadFileAsync
1 parent 54026bb commit ad73c46

6 files changed

Lines changed: 140 additions & 28 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Renci.SshNet
2+
{
3+
/// <summary>
4+
/// Provides the progress for a file download.
5+
/// </summary>
6+
public struct DownloadFileProgressReport
7+
{
8+
/// <summary>
9+
/// Gets the total number of bytes downloaded.
10+
/// </summary>
11+
public ulong TotalBytesDownloaded { get; internal set; }
12+
}
13+
}

src/Renci.SshNet/ISftpClient.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ public interface ISftpClient : IBaseClient
583583
/// </summary>
584584
/// <param name="path">The path to the remote file.</param>
585585
/// <param name="output">The <see cref="Stream"/> to write the file into.</param>
586-
/// <param name="downloadCallback">The download callback.</param>
586+
/// <param name="downloadProgress">The download progress.</param>
587587
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
588588
/// <returns>A <see cref="Task"/> that represents the asynchronous download operation.</returns>
589589
/// <exception cref="ArgumentNullException"><paramref name="output"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
@@ -593,7 +593,7 @@ public interface ISftpClient : IBaseClient
593593
/// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
594594
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
595595
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
596-
Task DownloadFileAsync(string path, Stream output, Action<ulong>? downloadCallback, CancellationToken cancellationToken = default);
596+
Task DownloadFileAsync(string path, Stream output, IProgress<DownloadFileProgressReport>? downloadProgress, CancellationToken cancellationToken = default);
597597

598598
/// <summary>
599599
/// Ends an asynchronous file downloading into the stream.
@@ -1150,12 +1150,29 @@ public interface ISftpClient : IBaseClient
11501150
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
11511151
Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default);
11521152

1153+
/// <summary>
1154+
/// Asynchronously uploads a <see cref="Stream"/> to a remote file path.
1155+
/// </summary>
1156+
/// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
1157+
/// <param name="path">The remote file path to write to.</param>
1158+
/// <param name="uploadProgress">The upload progress.</param>
1159+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
1160+
/// <returns>A <see cref="Task"/> that represents the asynchronous upload operation.</returns>
1161+
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
1162+
/// <exception cref="ArgumentException"><paramref name="path" /> is empty or contains only whitespace characters.</exception>
1163+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
1164+
/// <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>
1165+
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
1166+
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1167+
Task UploadFileAsync(Stream input, string path, IProgress<UploadFileProgressReport>? uploadProgress, CancellationToken cancellationToken = default);
1168+
11531169
/// <summary>
11541170
/// Asynchronously uploads a <see cref="Stream"/> to a remote file path.
11551171
/// </summary>
11561172
/// <param name="input">The <see cref="Stream"/> to write to the remote path.</param>
11571173
/// <param name="path">The remote file path to write to.</param>
11581174
/// <param name="canOverride">Whether the remote file can be overwritten if it already exists.</param>
1175+
/// <param name="uploadProgress">The upload progress.</param>
11591176
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
11601177
/// <returns>A <see cref="Task"/> that represents the asynchronous upload operation.</returns>
11611178
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="path"/> is <see langword="null"/>.</exception>
@@ -1164,7 +1181,7 @@ public interface ISftpClient : IBaseClient
11641181
/// <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>
11651182
/// <exception cref="SshException">An SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
11661183
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
1167-
Task UploadFileAsync(Stream input, string path, bool canOverride, CancellationToken cancellationToken = default);
1184+
Task UploadFileAsync(Stream input, string path, bool canOverride, IProgress<UploadFileProgressReport>? uploadProgress = null, CancellationToken cancellationToken = default);
11681185

11691186
/// <summary>
11701187
/// Writes the specified byte array to the specified file, and closes the file.

src/Renci.SshNet/SftpClient.cs

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -901,23 +901,30 @@ public void DownloadFile(string path, Stream output, Action<ulong>? downloadCall
901901
ArgumentNullException.ThrowIfNull(output);
902902
CheckDisposed();
903903

904+
IProgress<DownloadFileProgressReport>? downloadProgress = null;
905+
906+
if (downloadCallback != null)
907+
{
908+
downloadProgress = new Progress<DownloadFileProgressReport>(r => downloadCallback(r.TotalBytesDownloaded));
909+
}
910+
904911
InternalDownloadFile(
905912
path,
906913
output,
907914
asyncResult: null,
908-
downloadCallback,
915+
downloadProgress,
909916
isAsync: false,
910917
CancellationToken.None).GetAwaiter().GetResult();
911918
}
912919

913920
/// <inheritdoc />
914921
public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default)
915922
{
916-
return DownloadFileAsync(path, output, downloadCallback: null, cancellationToken);
923+
return DownloadFileAsync(path, output, downloadProgress: null, cancellationToken);
917924
}
918925

919926
/// <inheritdoc />
920-
public Task DownloadFileAsync(string path, Stream output, Action<ulong>? downloadCallback, CancellationToken cancellationToken = default)
927+
public Task DownloadFileAsync(string path, Stream output, IProgress<DownloadFileProgressReport>? downloadProgress, CancellationToken cancellationToken = default)
921928
{
922929
ArgumentException.ThrowIfNullOrWhiteSpace(path);
923930
ArgumentNullException.ThrowIfNull(output);
@@ -927,7 +934,7 @@ public Task DownloadFileAsync(string path, Stream output, Action<ulong>? downloa
927934
path,
928935
output,
929936
asyncResult: null,
930-
downloadCallback: downloadCallback,
937+
downloadProgress: downloadProgress,
931938
isAsync: true,
932939
cancellationToken);
933940
}
@@ -1000,6 +1007,13 @@ public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback?
10001007
ArgumentNullException.ThrowIfNull(output);
10011008
CheckDisposed();
10021009

1010+
IProgress<DownloadFileProgressReport>? downloadProgress = null;
1011+
1012+
if (downloadCallback != null)
1013+
{
1014+
downloadProgress = new Progress<DownloadFileProgressReport>(r => downloadCallback(r.TotalBytesDownloaded));
1015+
}
1016+
10031017
var asyncResult = new SftpDownloadAsyncResult(asyncCallback, state);
10041018

10051019
_ = DoDownloadAndSetResult();
@@ -1012,7 +1026,7 @@ await InternalDownloadFile(
10121026
path,
10131027
output,
10141028
asyncResult,
1015-
downloadCallback,
1029+
downloadProgress,
10161030
isAsync: true,
10171031
CancellationToken.None).ConfigureAwait(false);
10181032

@@ -1071,24 +1085,37 @@ public void UploadFile(Stream input, string path, bool canOverride, Action<ulong
10711085
flags |= Flags.CreateNew;
10721086
}
10731087

1088+
IProgress<UploadFileProgressReport>? uploadProgress = null;
1089+
1090+
if (uploadCallback != null)
1091+
{
1092+
uploadProgress = new Progress<UploadFileProgressReport>(r => uploadCallback(r.TotalBytesUploaded));
1093+
}
1094+
10741095
InternalUploadFile(
10751096
input,
10761097
path,
10771098
flags,
10781099
asyncResult: null,
1079-
uploadCallback,
1100+
uploadProgress,
10801101
isAsync: false,
10811102
CancellationToken.None).GetAwaiter().GetResult();
10821103
}
10831104

10841105
/// <inheritdoc />
10851106
public Task UploadFileAsync(Stream input, string path, CancellationToken cancellationToken = default)
10861107
{
1087-
return UploadFileAsync(input, path, canOverride: true, cancellationToken);
1108+
return UploadFileAsync(input, path, canOverride: true, uploadProgress: null, cancellationToken);
10881109
}
10891110

10901111
/// <inheritdoc />
1091-
public Task UploadFileAsync(Stream input, string path, bool canOverride, CancellationToken cancellationToken = default)
1112+
public Task UploadFileAsync(Stream input, string path, IProgress<UploadFileProgressReport>? uploadProgress, CancellationToken cancellationToken = default)
1113+
{
1114+
return UploadFileAsync(input, path, canOverride: true, uploadProgress, cancellationToken);
1115+
}
1116+
1117+
/// <inheritdoc />
1118+
public Task UploadFileAsync(Stream input, string path, bool canOverride, IProgress<UploadFileProgressReport>? uploadProgress = null, CancellationToken cancellationToken = default)
10921119
{
10931120
ArgumentNullException.ThrowIfNull(input);
10941121
ArgumentException.ThrowIfNullOrWhiteSpace(path);
@@ -1110,7 +1137,7 @@ public Task UploadFileAsync(Stream input, string path, bool canOverride, Cancell
11101137
path,
11111138
flags,
11121139
asyncResult: null,
1113-
uploadCallback: null,
1140+
uploadProgress,
11141141
isAsync: true,
11151142
cancellationToken);
11161143
}
@@ -1242,6 +1269,13 @@ public IAsyncResult BeginUploadFile(Stream input, string path, bool canOverride,
12421269
flags |= Flags.CreateNew;
12431270
}
12441271

1272+
IProgress<UploadFileProgressReport>? uploadProgress = null;
1273+
1274+
if (uploadCallback != null)
1275+
{
1276+
uploadProgress = new Progress<UploadFileProgressReport>(r => uploadCallback(r.TotalBytesUploaded));
1277+
}
1278+
12451279
var asyncResult = new SftpUploadAsyncResult(asyncCallback, state);
12461280

12471281
_ = DoUploadAndSetResult();
@@ -1255,7 +1289,7 @@ await InternalUploadFile(
12551289
path,
12561290
flags,
12571291
asyncResult,
1258-
uploadCallback,
1292+
uploadProgress,
12591293
isAsync: true,
12601294
CancellationToken.None).ConfigureAwait(false);
12611295

@@ -2201,7 +2235,7 @@ private List<FileInfo> InternalSynchronizeDirectories(string sourcePath, string
22012235
remoteFileName,
22022236
uploadFlag,
22032237
asyncResult: null,
2204-
uploadCallback: null,
2238+
uploadProgress: null,
22052239
isAsync: false,
22062240
CancellationToken.None).GetAwaiter().GetResult();
22072241
#pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks
@@ -2297,7 +2331,7 @@ private async Task InternalDownloadFile(
22972331
string path,
22982332
Stream output,
22992333
SftpDownloadAsyncResult? asyncResult,
2300-
Action<ulong>? downloadCallback,
2334+
IProgress<DownloadFileProgressReport>? downloadProgress,
23012335
bool isAsync,
23022336
CancellationToken cancellationToken)
23032337
{
@@ -2383,13 +2417,15 @@ private async Task InternalDownloadFile(
23832417

23842418
asyncResult?.Update(totalBytesRead);
23852419

2386-
if (downloadCallback is not null)
2420+
if (downloadProgress is not null)
23872421
{
23882422
// Copy offset to ensure it's not modified between now and execution of callback
2389-
var downloadOffset = totalBytesRead;
2423+
var report = new DownloadFileProgressReport()
2424+
{
2425+
TotalBytesDownloaded = totalBytesRead,
2426+
};
23902427

2391-
// Execute callback on different thread
2392-
ThreadAbstraction.ExecuteThread(() => { downloadCallback(downloadOffset); });
2428+
downloadProgress.Report(report);
23932429
}
23942430
}
23952431
}
@@ -2413,7 +2449,7 @@ private async Task InternalUploadFile(
24132449
string path,
24142450
Flags flags,
24152451
SftpUploadAsyncResult? asyncResult,
2416-
Action<ulong>? uploadCallback,
2452+
IProgress<UploadFileProgressReport>? uploadProgress,
24172453
bool isAsync,
24182454
CancellationToken cancellationToken)
24192455
{
@@ -2501,10 +2537,14 @@ private async Task InternalUploadFile(
25012537
asyncResult?.Update(writtenBytes);
25022538

25032539
// Call callback to report number of bytes written
2504-
if (uploadCallback is not null)
2540+
if (uploadProgress is not null)
25052541
{
2506-
// Execute callback on different thread
2507-
ThreadAbstraction.ExecuteThread(() => uploadCallback(writtenBytes));
2542+
UploadFileProgressReport report = new()
2543+
{
2544+
TotalBytesUploaded = writtenBytes,
2545+
};
2546+
2547+
uploadProgress.Report(report);
25082548
}
25092549
}
25102550
finally
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Renci.SshNet
2+
{
3+
/// <summary>
4+
/// Provides the progress for a file upload.
5+
/// </summary>
6+
public struct UploadFileProgressReport
7+
{
8+
/// <summary>
9+
/// Gets the total number of bytes uploaded.
10+
/// </summary>
11+
public ulong TotalBytesUploaded { get; internal set; }
12+
}
13+
}

test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void Test_Sftp_EndDownloadFile_Invalid_Async_Handle()
127127

128128
[TestMethod]
129129
[TestCategory("Sftp")]
130-
public async Task Test_Sftp_DownloadFileAsync_DownloadCallback()
130+
public async Task Test_Sftp_DownloadFileAsync_DownloadProgress()
131131
{
132132
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
133133
{
@@ -138,15 +138,15 @@ public async Task Test_Sftp_DownloadFileAsync_DownloadCallback()
138138
await sftp.UploadFileAsync(File.OpenRead(filename), "test123");
139139
using ManualResetEventSlim finalCallbackCalledEvent = new();
140140

141-
void Callback(ulong totalBytesRead)
141+
IProgress<DownloadFileProgressReport> progress = new Progress<DownloadFileProgressReport>(r =>
142142
{
143-
if ((int)totalBytesRead == testFileSizeMB * 1024 * 1024)
143+
if ((int)r.TotalBytesDownloaded == testFileSizeMB * 1024 * 1024)
144144
{
145145
finalCallbackCalledEvent.Set();
146146
}
147-
}
147+
});
148148

149-
await sftp.DownloadFileAsync("test123", new MemoryStream(), Callback, CancellationToken.None);
149+
await sftp.DownloadFileAsync("test123", new MemoryStream(), progress, CancellationToken.None);
150150

151151
// since the callback is queued to the thread pool, wait for the event.
152152
bool callbackCalled = finalCallbackCalledEvent.Wait(5000);

test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,5 +453,34 @@ public void Test_Sftp_EndUploadFile_Invalid_Async_Handle()
453453
Assert.ThrowsExactly<ArgumentException>(() => sftp.EndUploadFile(async1));
454454
}
455455
}
456+
457+
[TestMethod]
458+
[TestCategory("Sftp")]
459+
public async Task Test_Sftp_UploadFileAsync_UploadProgress()
460+
{
461+
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
462+
{
463+
await sftp.ConnectAsync(CancellationToken.None);
464+
var filename = Path.GetTempFileName();
465+
int testFileSizeMB = 1;
466+
CreateTestFile(filename, testFileSizeMB);
467+
using var fileStream = File.OpenRead(filename);
468+
using ManualResetEventSlim finalCallbackCalledEvent = new();
469+
470+
IProgress<UploadFileProgressReport> progress = new Progress<UploadFileProgressReport>(r =>
471+
{
472+
if ((int)r.TotalBytesUploaded == testFileSizeMB * 1024 * 1024)
473+
{
474+
finalCallbackCalledEvent.Set();
475+
}
476+
});
477+
478+
await sftp.UploadFileAsync(fileStream, "test", progress);
479+
480+
// since the callback is queued to the thread pool, wait for the event.
481+
bool callbackCalled = finalCallbackCalledEvent.Wait(5000);
482+
Assert.IsTrue(callbackCalled);
483+
}
484+
}
456485
}
457486
}

0 commit comments

Comments
 (0)