Skip to content

Instantly share code, notes, and snippets.

@rquackenbush
Created October 27, 2017 16:16
Show Gist options
  • Save rquackenbush/5ebf84aab5d8a545895a0beafcb0f390 to your computer and use it in GitHub Desktop.
Save rquackenbush/5ebf84aab5d8a545895a0beafcb0f390 to your computer and use it in GitHub Desktop.
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