ln.objects/ng/storage/SegmentedFile.cs

309 lines
8.0 KiB
C#

// /**
// * File: SegmentedFile.cs
// * Author: haraldwolff
// *
// * This file and it's content is copyrighted by the Author and / or copyright holder.
// * Any use wihtout proper permission is illegal and may lead to legal actions.
// *
// *
// **/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ln.logging;
using ln.types.btree;
namespace ln.types.odb.ng.storage
{
/**
* SegmentedFile
* ----------
* 0000 4 MAGIC Bytes
* 0004 4 Version
* 0008 8 LastCloseTimestamp
* 0010 4 FirstOffset
* 0014 4 GranularWidth
* 0018 8 Reserved 0
*
**/
public class SegmentedFile
{
public static byte[] MagicBytes { get; } = new byte[] { 0x0F, 0x0E, 0x0D, 0x0A };
public String FileName { get; }
public int FileVersion { get; private set; }
public long LastCloseTimestamp { get; private set; }
public int FirstOffset { get; private set; }
public int GranularWidth { get; private set; } = 12;
public int GranularityMask => (1 << GranularWidth) - 1;
public int AppendOffset { get; private set; }
public IEnumerable<Segment> Segments => segments;
MappingBTree<long, Segment> segments = new MappingBTree<long, Segment>((s) => s.Offset);
FileStream fileStream;
public SegmentedFile(string fileName)
{
FileName = fileName;
}
public SegmentedFile(string fileName,int granularWidth)
:this(fileName)
{
GranularWidth = granularWidth;
}
private void AssertOpen()
{
if (fileStream == null)
throw new IOException("FSStorage not opened");
}
private void CheckGranularity(ref int i){ i = (i + GranularityMask) & ~GranularityMask; }
public bool IsOpen => (fileStream != null);
public bool Open()
{
if (!IsOpen)
{
try
{
fileStream = new FileStream(FileName, FileMode.OpenOrCreate);
if (fileStream.Length == 0)
{
FileVersion = 0;
LastCloseTimestamp = 0;
FirstOffset = (1 << GranularWidth);
if (FirstOffset < 0x20)
throw new NotSupportedException("Granularity too small");
AppendOffset = FirstOffset;
Close();
return Open();
}
else
{
if (!fileStream.ReadBytes(4).SequenceEqual(MagicBytes))
throw new IOException("Magic bytes do not match");
FileVersion = fileStream.ReadInteger();
LastCloseTimestamp = fileStream.ReadLong();
FirstOffset = fileStream.ReadInteger();
GranularWidth = fileStream.ReadInteger();
Scan();
fileStream.Position = 8;
fileStream.WriteLong(0);
fileStream.Flush();
}
}
catch (Exception e)
{
Logging.Log(e);
if (fileStream != null)
{
fileStream.Close();
fileStream.Dispose();
fileStream = null;
}
return false;
}
return true;
}
return false;
}
public Segment Append(Guid id,byte[] payload) => Append(id, payload.Length, payload);
public Segment Append(Guid id, int dataSize) => Append(id, dataSize);
public Segment Append(Guid id, int dataSize, byte[] payload)
{
dataSize += Segment.HeaderSize;
CheckGranularity(ref dataSize);
Segment segment = new Segment(AppendOffset, dataSize) { ID = id, };
Write(segment, payload);
segments.Add(segment);
AppendOffset = segment.NextOffset;
return segment;
}
public Segment Join(Segment a,Segment b)
{
if (a.NextOffset != b.Offset)
throw new ArgumentException("Segments to join are not siblings");
a.Size += b.Size;
WriteSegmentHead(a);
segments.Remove(b);
return a;
}
public Segment Split(Segment segment,int dataSize)
{
int requestedSize = dataSize + Segment.HeaderSize;
CheckGranularity(ref requestedSize);
if (requestedSize < segment.Size)
{
Segment split = new Segment(segment.Offset + requestedSize,segment.Size - requestedSize);
segment.Size = requestedSize;
segments.Add(split);
WriteSegmentHead(split);
WriteSegmentHead(segment);
return split;
}
return null;
}
public byte[] Read(Segment segment)
{
fileStream.Position = segment.PayloadOffset;
return fileStream.ReadBytes(segment.PayloadSize);
}
private void WriteSegmentHead(Segment segment)
{
fileStream.Position = segment.Offset;
fileStream.WriteInteger(segment.Size);
fileStream.WriteBytes(segment.ID.ToByteArray());
fileStream.WriteDouble(segment.TimeStamp.ToUnixTimeMilliseconds());
}
public void Write(Segment segment,byte[] bytes)
{
AssertOpen();
if (bytes.Length > (segment.PayloadSize))
throw new ArgumentOutOfRangeException(nameof(bytes));
segment.TimeStamp = DateTime.Now;
WriteSegmentHead(segment);
fileStream.Position = segment.PayloadOffset;
fileStream.WriteBytes(bytes);
fileStream.WriteBytes(new byte[segment.PayloadSize - bytes.Length]);
}
/**
* Position fileStream to offset, read Segment Header and construct a Segment instance to return
**/
private Segment ScanSegment(int offset)
{
fileStream.Position = offset;
int size = fileStream.ReadInteger();
byte[] id = fileStream.ReadBytes(16);
double timestamp = fileStream.ReadDouble();
return new Segment(offset, size, DateTimeExtensions.FromUnixTimeMilliseconds(timestamp)) { ID = new Guid(id), };
}
/**
* Start at First Segment Offset and scan for all Segments in file
**/
private void Scan()
{
int offset = FirstOffset;
Segment segment = null;
while (offset < fileStream.Length)
{
segment = ScanSegment(offset);
segments.Add(segment);
offset = segment.NextOffset;
}
AppendOffset = offset;
}
public void Close()
{
lock (this){
AssertOpen();
fileStream.Position = 0;
fileStream.WriteBytes(MagicBytes);
fileStream.WriteInteger(FileVersion);
LastCloseTimestamp = (long)DateTime.Now.ToUnixTimeMilliseconds();
fileStream.WriteLong(LastCloseTimestamp);
fileStream.WriteInteger(FirstOffset);
fileStream.WriteInteger(GranularWidth);
fileStream.Close();
fileStream.Dispose();
fileStream = null;
}
}
public void Sync()
{
lock (this)
{
fileStream.Flush();
}
}
public class Segment
{
public static readonly int HeaderSize = 32;
public int Offset { get; }
public int Size { get; set; }
public int PayloadOffset => Offset + HeaderSize;
public int PayloadSize => Size - HeaderSize;
public Guid ID { get; set; }
public DateTime TimeStamp { get; set; }
public int NextOffset => Offset + Size;
public Segment(int offset, int size)
{
Offset = offset;
Size = size;
}
public Segment(int offset, int size,DateTime timestamp)
:this(offset,size)
{
TimeStamp = timestamp;
}
public Segment Split(int splitSize)
{
if (splitSize >= Size)
throw new ArgumentOutOfRangeException(nameof(splitSize));
Segment splitArea = new Segment(Offset + Size - splitSize, splitSize);
Size -= splitSize;
return splitArea;
}
public override string ToString()
{
return string.Format("[StorageArea Offset=0x{0:x8} Size=0x{1:x8}]", Offset, Size);
}
}
}
}