|
|
@@ -0,0 +1,163 @@
|
|
|
+using Renci.SshNet.Abstractions;
|
|
|
+using Renci.SshNet.Common;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Threading;
|
|
|
+
|
|
|
+namespace Renci.SshNet.Sftp
|
|
|
+{
|
|
|
+ internal class SftpFileReader
|
|
|
+ {
|
|
|
+ private const int MaxReadAhead = 15;
|
|
|
+
|
|
|
+ private readonly byte[] _handle;
|
|
|
+ private readonly ISftpSession _sftpSession;
|
|
|
+ private SemaphoreLight _semaphore;
|
|
|
+ private bool _isCompleted;
|
|
|
+ private uint _chunkLength;
|
|
|
+ private int _readAheadChunkIndex;
|
|
|
+ private int _nextChunkIndex;
|
|
|
+ private ulong _readAheadOffset;
|
|
|
+ private ulong _offset;
|
|
|
+ private ulong _fileSize;
|
|
|
+ private readonly IDictionary<int, BufferedRead> _queue;
|
|
|
+ private readonly object _readLock;
|
|
|
+
|
|
|
+ public SftpFileReader(byte[] handle, ISftpSession sftpSession)
|
|
|
+ {
|
|
|
+ _handle = handle;
|
|
|
+ _sftpSession = sftpSession;
|
|
|
+ _chunkLength = 32 * 1024; // TODO !
|
|
|
+ _semaphore = new SemaphoreLight(MaxReadAhead);
|
|
|
+ _queue = new Dictionary<int, BufferedRead>(MaxReadAhead);
|
|
|
+ _readLock = new object();
|
|
|
+
|
|
|
+ _fileSize = (ulong)_sftpSession.RequestFStat(_handle).Size;
|
|
|
+
|
|
|
+ ThreadAbstraction.ExecuteThread(() =>
|
|
|
+ {
|
|
|
+ while (!_isCompleted)
|
|
|
+ {
|
|
|
+ // we reach one chunk beyond the file size to get an EOF
|
|
|
+ if (_readAheadOffset > _fileSize)
|
|
|
+ break;
|
|
|
+
|
|
|
+ // TODO implement cancellation!?
|
|
|
+ _semaphore.Wait();
|
|
|
+
|
|
|
+ // start reading next chunk
|
|
|
+ _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkLength, ReadCompleted,
|
|
|
+ new BufferedRead(_readAheadChunkIndex, _readAheadOffset));
|
|
|
+
|
|
|
+ // advance read-ahead offset
|
|
|
+ _readAheadOffset += _chunkLength;
|
|
|
+
|
|
|
+ _readAheadChunkIndex++;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ public byte[] Read()
|
|
|
+ {
|
|
|
+ lock (_readLock)
|
|
|
+ {
|
|
|
+ BufferedRead nextChunk;
|
|
|
+
|
|
|
+ while (!_queue.TryGetValue(_nextChunkIndex, out nextChunk) && !_isCompleted)
|
|
|
+ Monitor.Wait(_readLock);
|
|
|
+
|
|
|
+ if (_isCompleted)
|
|
|
+ return new byte[0];
|
|
|
+
|
|
|
+ if (nextChunk.Offset == _offset)
|
|
|
+ {
|
|
|
+ var data = nextChunk.Data;
|
|
|
+ _offset += (ulong) data.Length;
|
|
|
+
|
|
|
+ // remove processed chunk
|
|
|
+ _queue.Remove(_nextChunkIndex);
|
|
|
+ // move to next chunk
|
|
|
+ _nextChunkIndex++;
|
|
|
+ // allow read-ahead of a new chunk
|
|
|
+ _semaphore.Release();
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ // when the server returned less bytes than requested (for the previous chunk)
|
|
|
+ // we'll synchronously request the remaining data
|
|
|
+
|
|
|
+ var catchUp = new byte[nextChunk.Offset - _offset];
|
|
|
+ var bytesCaughtUp = 0L;
|
|
|
+
|
|
|
+ while (bytesCaughtUp < catchUp.Length)
|
|
|
+ {
|
|
|
+ // TODO: break loop and interrupt blocking wait in case of exception
|
|
|
+ var read = _sftpSession.RequestRead(_handle, _offset, (uint) catchUp.Length);
|
|
|
+ if (read.Length == 0)
|
|
|
+ {
|
|
|
+ // break in loop in "read-ahead" thread (once a blocking wait is interrupted)
|
|
|
+ _isCompleted = true;
|
|
|
+ // interrupt blocking wait in "read-ahead" thread
|
|
|
+ lock (_readLock)
|
|
|
+ Monitor.PulseAll(_readLock);
|
|
|
+ // signal failure
|
|
|
+ throw new SshException("Unexpectedly reached end of file.");
|
|
|
+ }
|
|
|
+
|
|
|
+ bytesCaughtUp += read.Length;
|
|
|
+ _offset += (ulong) bytesCaughtUp;
|
|
|
+ }
|
|
|
+
|
|
|
+ return catchUp;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ReadCompleted(IAsyncResult result)
|
|
|
+ {
|
|
|
+ var readAsyncResult = result as SftpReadAsyncResult;
|
|
|
+ if (readAsyncResult == null)
|
|
|
+ return;
|
|
|
+
|
|
|
+ var data = readAsyncResult.EndInvoke();
|
|
|
+ if (data.Length == 0)
|
|
|
+ {
|
|
|
+ Console.WriteLine("EOF");
|
|
|
+
|
|
|
+ _isCompleted = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var bufferedRead = (BufferedRead)readAsyncResult.AsyncState;
|
|
|
+ bufferedRead.Complete(data);
|
|
|
+ _queue.Add(bufferedRead.ChunkIndex, bufferedRead);
|
|
|
+ }
|
|
|
+
|
|
|
+ // signal that a chunk has been read or EOF has been reached;
|
|
|
+ // in both cases, we want to unblock the "read-ahead" thread
|
|
|
+ lock (_readLock)
|
|
|
+ {
|
|
|
+ Monitor.Pulse(_readLock);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private class BufferedRead
|
|
|
+ {
|
|
|
+ public int ChunkIndex { get; private set; }
|
|
|
+
|
|
|
+ public byte[] Data { get; private set; }
|
|
|
+
|
|
|
+ public ulong Offset { get; private set; }
|
|
|
+
|
|
|
+ public BufferedRead(int chunkIndex, ulong offset)
|
|
|
+ {
|
|
|
+ ChunkIndex = chunkIndex;
|
|
|
+ Offset = offset;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void Complete(byte[] data)
|
|
|
+ {
|
|
|
+ Data = data;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|