diff --git a/src/Renci.SshNet/IServiceFactory.cs b/src/Renci.SshNet/IServiceFactory.cs
index 8fea7decc..ef6e7afa0 100644
--- a/src/Renci.SshNet/IServiceFactory.cs
+++ b/src/Renci.SshNet/IServiceFactory.cs
@@ -90,10 +90,11 @@ internal partial interface IServiceFactory
/// The file to read.
/// The SFTP session to use.
/// The size of buffer.
+ /// The offset to resume from.
///
/// An .
///
- ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
+ ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize, ulong offset = 0);
///
/// Creates a new instance.
diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs
index 7c67fbf16..e497f85e4 100644
--- a/src/Renci.SshNet/ServiceFactory.cs
+++ b/src/Renci.SshNet/ServiceFactory.cs
@@ -127,10 +127,11 @@ public INetConfSession CreateNetConfSession(ISession session, int operationTimeo
/// The file to read.
/// The SFTP session to use.
/// The size of buffer.
+ /// The offset to resume from.
///
/// An .
///
- public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize)
+ public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize, ulong offset = 0)
{
const int defaultMaxPendingReads = 10;
@@ -152,7 +153,9 @@ public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSe
{
var fileAttributes = sftpSession.EndLStat(statAsyncResult);
fileSize = fileAttributes.Size;
- maxPendingReads = Math.Min(100, (int)Math.Ceiling((double)fileAttributes.Size / chunkSize) + 1);
+
+ // calculate maxPendingReads based on remaining size, not total filesize (needed for resume support)
+ maxPendingReads = Math.Min(100, (int) Math.Ceiling((double)(fileSize - (long)offset) / chunkSize) + 1);
}
catch (SshException ex)
{
@@ -162,7 +165,7 @@ public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSe
DiagnosticAbstraction.Log(string.Format("Failed to obtain size of file. Allowing maximum {0} pending reads: {1}", maxPendingReads, ex));
}
- return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
+ return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize, offset);
}
///
diff --git a/src/Renci.SshNet/Sftp/ISftpSession.cs b/src/Renci.SshNet/Sftp/ISftpSession.cs
index e49936b7a..a5329a701 100644
--- a/src/Renci.SshNet/Sftp/ISftpSession.cs
+++ b/src/Renci.SshNet/Sftp/ISftpSession.cs
@@ -494,10 +494,11 @@ void RequestWrite(byte[] handle,
/// The maximum number of bytes to read with each chunk.
/// The maximum number of pending reads.
/// The size of the file or when the size could not be determined.
+ /// The offset to resume from.
///
/// An for reading the content of the file represented by the
/// specified .
///
- ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize);
+ ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize, ulong offset = 0);
}
}
diff --git a/src/Renci.SshNet/Sftp/SftpFileReader.cs b/src/Renci.SshNet/Sftp/SftpFileReader.cs
index f47a941d9..f31a24b6b 100644
--- a/src/Renci.SshNet/Sftp/SftpFileReader.cs
+++ b/src/Renci.SshNet/Sftp/SftpFileReader.cs
@@ -56,7 +56,8 @@ internal sealed class SftpFileReader : ISftpFileReader
/// The size of a individual read-ahead chunk.
/// The maximum number of pending reads.
/// The size of the file, if known; otherwise, .
- public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
+ /// The offset to resume from.
+ public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize, ulong offset = 0)
{
_handle = handle;
_sftpSession = sftpSession;
@@ -69,6 +70,11 @@ public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, i
_disposingWaitHandle = new ManualResetEvent(initialState: false);
_waitHandles = _sftpSession.CreateWaitHandleArray(_disposingWaitHandle, _semaphore.AvailableWaitHandle);
+ // When resuming a download (offset > 0), set the initial offset of the remote file to
+ // the same offset as the local output file. Read-ahead also starts at the same offset.
+ _offset = offset;
+ _readAheadOffset = offset;
+
StartReadAhead();
}
diff --git a/src/Renci.SshNet/Sftp/SftpSession.cs b/src/Renci.SshNet/Sftp/SftpSession.cs
index 7943d9a3c..0845214c7 100644
--- a/src/Renci.SshNet/Sftp/SftpSession.cs
+++ b/src/Renci.SshNet/Sftp/SftpSession.cs
@@ -239,13 +239,14 @@ public async Task GetCanonicalPathAsync(string path, CancellationToken c
/// The maximum number of bytes to read with each chunk.
/// The maximum number of pending reads.
/// The size of the file or when the size could not be determined.
+ /// The offset to resume from.
///
/// An for reading the content of the file represented by the
/// specified .
///
- public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
+ public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize, ulong offset = 0)
{
- return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
+ return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize, offset);
}
internal string GetFullRemotePath(string path)
diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs
index b82e605cc..45a374533 100644
--- a/src/Renci.SshNet/SftpClient.cs
+++ b/src/Renci.SshNet/SftpClient.cs
@@ -979,7 +979,12 @@ public void UploadFile(Stream input, string path, bool canOverride, Action 0)
+ {
+ // if the local stream position is not zero, open the remote file in APPEND mode to resume upload
+ flags = Flags.Write | Flags.Append;
+ }
+ else if (canOverride)
{
flags |= Flags.CreateNewOrOpen;
}
@@ -1117,7 +1122,12 @@ public IAsyncResult BeginUploadFile(Stream input, string path, bool canOverride,
var flags = Flags.Write | Flags.Truncate;
- if (canOverride)
+ if (input.Position > 0)
+ {
+ // if the local stream position is not zero, open the remote file in APPEND mode to resume upload
+ flags = Flags.Write | Flags.Append;
+ }
+ else if (canOverride)
{
flags |= Flags.CreateNewOrOpen;
}
@@ -2432,7 +2442,8 @@ private void InternalUploadFile(Stream input, string path, Flags flags, SftpUplo
var handle = _sftpSession.RequestOpen(fullPath, flags);
- ulong offset = 0;
+ // Set the initial offset of the remote file to the same as the local file to allow resuming
+ var offset = (ulong)input.Position;
// create buffer of optimal length
var buffer = new byte[_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle)];
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs
index ce5c7b04d..f705d0459 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs
@@ -62,7 +62,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Throws(new SshException());
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 10, null))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 10, null, 0))
.Returns(_sftpFileReaderMock.Object);
}
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs
index a869f48f8..9a3a3adc3 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs
@@ -66,7 +66,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Returns(_fileAttributes);
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize, 0))
.Returns(_sftpFileReaderMock.Object);
}
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs
index 29a0c9c71..aa9777aad 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs
@@ -66,7 +66,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Returns(_fileAttributes);
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize, 0))
.Returns(_sftpFileReaderMock.Object);
}
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs
index 47a446c17..aeec3b510 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs
@@ -66,7 +66,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Returns(_fileAttributes);
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 6, _fileSize))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 6, _fileSize, 0))
.Returns(_sftpFileReaderMock.Object);
}
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs
index b2202b183..9b85d2895 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs
@@ -66,7 +66,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Returns(_fileAttributes);
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize, 0))
.Returns(_sftpFileReaderMock.Object);
}
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs
index c0439cba9..30283186d 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs
@@ -66,7 +66,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Returns(_fileAttributes);
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize, 0))
.Returns(_sftpFileReaderMock.Object);
}
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanMaxPendingReadsTimesChunkSize.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanMaxPendingReadsTimesChunkSize.cs
index 105085a25..399ac41ea 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanMaxPendingReadsTimesChunkSize.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanMaxPendingReadsTimesChunkSize.cs
@@ -68,7 +68,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Returns(_fileAttributes);
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, _maxPendingReads, _fileSize))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, _maxPendingReads, _fileSize, 0))
.Returns(_sftpFileReaderMock.Object);
}
diff --git a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs
index 068865800..afb5eb81d 100644
--- a/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs
+++ b/test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs
@@ -66,7 +66,7 @@ private void SetupMocks()
.Setup(p => p.EndLStat(_statAsyncResult))
.Returns(_fileAttributes);
_sftpSessionMock.InSequence(seq)
- .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 1, _fileSize))
+ .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 1, _fileSize, 0))
.Returns(_sftpFileReaderMock.Object);
}