Created
October 27, 2017 16:16
-
-
Save rquackenbush/5ebf84aab5d8a545895a0beafcb0f390 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using CaptiveAire.Scada.Module.Base.Extensions; | |
namespace CaptiveAire.Scada.Module.Base | |
{ | |
/// <summary> | |
/// Treats multiple files as a single stream. Designed to be used on a large file that was downloaded in chunks. | |
/// </summary> | |
/// <remarks> | |
/// Based loosely on http://www.dib0.nl/code/495-handling-multiple-files-as-one-stream-in-c | |
/// Assumptions: | |
/// - The source files won't be changed after instantiating this class | |
/// </remarks> | |
public class ConcatenatingStream : Stream | |
{ | |
private readonly List<FileToken> _files; | |
private readonly long _length; | |
private long _position; | |
private int _currentFileIndex; | |
private Stream _currentStream; | |
public ConcatenatingStream(string[] filenames) | |
{ | |
if (filenames == null) throw new ArgumentNullException(nameof(filenames)); | |
//Create space of the files | |
_files = new List<FileToken>(filenames.Length); | |
//Keep track of where we're in the virtual concatenated stream. | |
long currentPosition = 0; | |
//Consider each file | |
foreach (var filename in filenames) | |
{ | |
//Get the information for this file | |
var fileInfo = new FileInfo(filename); | |
//Get the size of the file | |
var size = fileInfo.Length; | |
//Add it to the list | |
_files.Add(new FileToken(filename, currentPosition, size)); | |
//Adjust the current position | |
currentPosition += size; | |
//Adjust the total length | |
_length += size; | |
} | |
if (_files.Any()) | |
{ | |
//Start at the beginning! | |
CurrentStream = _files[0].Open(); | |
} | |
} | |
private Stream CurrentStream | |
{ | |
get { return _currentStream; } | |
set | |
{ | |
if (value == null) throw new ArgumentNullException(nameof(value)); | |
if (_currentStream != value) | |
{ | |
_currentStream?.Dispose(); | |
} | |
_currentStream = value; | |
} | |
} | |
private bool MoveNext() | |
{ | |
if (_files.Count == 0) | |
return false; | |
if (_currentFileIndex < _files.Count - 1) | |
{ | |
_currentFileIndex++; | |
CurrentStream = _files[_currentFileIndex].Open(); | |
return true; | |
} | |
return false; | |
} | |
public override void Flush() | |
{ | |
//Nothing to do here | |
} | |
private long SeekAbsolute(long position) | |
{ | |
if (position < 0) | |
throw new IOException($"Cannot seek to offset of {position}."); | |
if (position > (_length - 1)) | |
throw new IOException($"Cannot seek past the end of the stream of {Length} bytes to position {position}"); | |
if (_position == position) | |
return position; | |
//Get the file index of the new file | |
var fileIndex = GetFileIndex(position); | |
//Get the file | |
var file = _files[fileIndex]; | |
//Check to see if we have to change source files | |
if (fileIndex != _currentFileIndex) | |
{ | |
CurrentStream = file.Open(); | |
//Update the file index | |
_currentFileIndex = fileIndex; | |
} | |
//Seek to the relative position we're looking for in this file | |
CurrentStream.Seek(position - file.Start, SeekOrigin.Begin); | |
//Update the position | |
_position = position; | |
return position; | |
} | |
public override long Seek(long offset, SeekOrigin origin) | |
{ | |
switch (origin) | |
{ | |
case SeekOrigin.Begin: | |
return SeekAbsolute(offset); | |
case SeekOrigin.Current: | |
return SeekAbsolute(_position + offset); | |
case SeekOrigin.End: | |
return SeekAbsolute(_length + offset); | |
default: | |
throw new ArgumentOutOfRangeException(nameof(origin), $"Unrecongnized SeekOrigin {origin}."); | |
} | |
} | |
public override void SetLength(long value) | |
{ | |
throw new NotSupportedException(); | |
} | |
public override int Read(byte[] buffer, int offset, int count) | |
{ | |
//Check to see if we're done | |
if (!_files.Any()) | |
return 0; | |
//If we're at the end, return 0 | |
if (_position >= _length) | |
return 0; | |
int result = 0; | |
int buffPosition = offset; | |
while (result < count) | |
{ | |
//Read from the source stream | |
int bytesRead = CurrentStream.Read(buffer, buffPosition, count - result); | |
//Increment indexes | |
result += bytesRead; | |
buffPosition += bytesRead; | |
_position += bytesRead; | |
//Check to see if we got all the requested bytes | |
if (result < count) | |
{ | |
if (!MoveNext()) | |
{ | |
return result; | |
} | |
} | |
} | |
return result; | |
} | |
public override void Write(byte[] buffer, int offset, int count) | |
{ | |
throw new NotSupportedException(); | |
} | |
public override bool CanRead | |
{ | |
get { return true; } | |
} | |
public override bool CanSeek | |
{ | |
get { return true; } | |
} | |
public override bool CanWrite | |
{ | |
get { return false; } | |
} | |
public override long Length | |
{ | |
get { return _length; } | |
} | |
public override long Position | |
{ | |
get { return _position; } | |
set { SeekAbsolute(value); } | |
} | |
public override int ReadTimeout { get; set; } | |
public override int WriteTimeout { get; set; } | |
/// <summary> | |
/// Gets the file of the index that contains the specified position. | |
/// </summary> | |
/// <param name="position"></param> | |
/// <returns></returns> | |
private int GetFileIndex(long position) | |
{ | |
int? result = _files.IndexOfOrNull(f => f.ContainsPosition(position)); | |
if (result == null) | |
throw new IOException($"There is no source file at position {position}"); | |
return result.Value; | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
base.Dispose(disposing); | |
CurrentStream.Dispose(); | |
} | |
private class FileToken | |
{ | |
private readonly string _path; | |
private readonly long _start; | |
private readonly long _length; | |
public FileToken(string path, long start, long length) | |
{ | |
if (path == null) throw new ArgumentNullException(nameof(path)); | |
if (length <= 0) throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than 0."); | |
_path = path; | |
_start = start; | |
_length = length; | |
} | |
public long Start | |
{ | |
get { return _start; } | |
} | |
public long Length | |
{ | |
get { return _length; } | |
} | |
public long End | |
{ | |
get { return Start + Length - 1; } | |
} | |
public bool ContainsPosition(long position) | |
{ | |
return position >= Start && position <= End; | |
} | |
public Stream Open() | |
{ | |
return File.Open(_path, FileMode.Open, FileAccess.Read, FileShare.Read); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment