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); }