293 lines
8.9 KiB
C#
293 lines
8.9 KiB
C#
// /**
|
|
// * File: FSSTorage.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 System.Xml.Serialization.Advanced;
|
|
using ln.logging;
|
|
|
|
namespace ln.types.odb.ng.storage
|
|
{
|
|
/**
|
|
* FSStorage
|
|
*
|
|
* Directory Layout:
|
|
*
|
|
* <BasePath>/<StorageName>
|
|
* /data.odb Serialized Document Data
|
|
* /data.idx Serialized Lookup Index for Documents and Free Areas
|
|
*
|
|
* data.odb
|
|
* ----------
|
|
* 0000 4 MAGIC Bytes
|
|
* 0004 4 Version
|
|
* 0008 8 LastCloseTimestamp
|
|
* 0010 4 FirstOffset
|
|
* 0014 4 GranularWidth
|
|
* 0018 8 Reserved 0
|
|
*
|
|
**/
|
|
public class FSStorage : IStorage
|
|
{
|
|
public byte[] MagicBytes { get; } = new byte[] { 0x0F, 0x0E, 0x0D, 0x0A };
|
|
|
|
public String StoragePath { get; }
|
|
public String StorageName { 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; }
|
|
|
|
StorageAreaContainer storageAreas = new StorageAreaContainer();
|
|
Dictionary<Guid, StorageArea> documentAreas = new Dictionary<Guid, StorageArea>();
|
|
|
|
FileStream fileStream;
|
|
|
|
public FSStorage(string storagePath)
|
|
{
|
|
StoragePath = storagePath;
|
|
StorageName = Path.GetFileName(storagePath);
|
|
}
|
|
public FSStorage(string storagePath,int granularWidth)
|
|
:this(storagePath)
|
|
{
|
|
GranularWidth = granularWidth;
|
|
}
|
|
|
|
private void AssertOpen()
|
|
{
|
|
if (fileStream == null)
|
|
throw new IOException("FSStorage not opened");
|
|
}
|
|
|
|
public bool IsOpen => (fileStream != null);
|
|
public bool Open()
|
|
{
|
|
if (!IsOpen)
|
|
{
|
|
try
|
|
{
|
|
if (!Directory.Exists(StoragePath))
|
|
Directory.CreateDirectory(StoragePath);
|
|
|
|
fileStream = new FileStream(Path.Combine(StoragePath, "data.odb"), 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();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logging.Log(e);
|
|
if (fileStream != null)
|
|
{
|
|
fileStream.Close();
|
|
fileStream.Dispose();
|
|
fileStream = null;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void Scan()
|
|
{
|
|
int offset = FirstOffset;
|
|
|
|
while (offset < fileStream.Length)
|
|
{
|
|
fileStream.Position = offset;
|
|
StorageArea storageArea = new StorageArea(offset, fileStream.ReadInteger());
|
|
Guid documentID = new Guid(fileStream.ReadBytes(16));
|
|
if (Guid.Empty.Equals(documentID))
|
|
storageAreas.Push(storageArea);
|
|
else
|
|
{
|
|
if (documentAreas.ContainsKey(documentID))
|
|
{
|
|
Document previousDoc = LoadDocument(documentAreas[documentID]);
|
|
Document currentDoc = LoadDocument(storageArea);
|
|
|
|
if (previousDoc.StorageTimeStamp < currentDoc.StorageTimeStamp)
|
|
{
|
|
WriteStorageArea(documentAreas[documentID]);
|
|
storageAreas.Push(documentAreas[documentID]);
|
|
documentAreas[documentID] = storageArea;
|
|
}
|
|
else
|
|
{
|
|
WriteStorageArea(storageArea);
|
|
storageAreas.Push(storageArea);
|
|
}
|
|
} else
|
|
{
|
|
documentAreas.Add(documentID, storageArea);
|
|
}
|
|
}
|
|
offset = storageArea.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 IEnumerable<Guid> GetDocumentIDs()
|
|
{
|
|
lock (this)
|
|
{
|
|
return documentAreas.Keys.ToArray();
|
|
}
|
|
}
|
|
|
|
public Document Load(Guid documentID)
|
|
{
|
|
lock (this)
|
|
{
|
|
if (!documentAreas.ContainsKey(documentID))
|
|
throw new KeyNotFoundException();
|
|
|
|
StorageArea storageArea = documentAreas[documentID];
|
|
return LoadDocument(storageArea);
|
|
}
|
|
}
|
|
|
|
private Document LoadDocument(StorageArea storageArea)
|
|
{
|
|
fileStream.Position = storageArea.Offset + 4;
|
|
Guid documentID = new Guid(fileStream.ReadBytes(16));
|
|
byte[] storageBytes = fileStream.ReadBytes(storageArea.Size - 20);
|
|
|
|
return new Document(documentID, storageBytes);
|
|
}
|
|
|
|
public void Save(Document document)
|
|
{
|
|
lock (this)
|
|
{
|
|
document.StorageTimeStamp = DateTime.Now;
|
|
byte[] storageBytes = document.ToStorage();
|
|
StorageArea storageArea = storageAreas.Pop(storageBytes.Length + 20);
|
|
if (storageArea == null)
|
|
storageArea = AppendStorageArea(storageBytes.Length + 20);
|
|
|
|
int neededSize = storageBytes.Length + 20;
|
|
CheckGranularity(ref neededSize);
|
|
|
|
if (storageArea.Size > neededSize)
|
|
{
|
|
StorageArea splitArea = storageArea.Split(storageArea.Size - neededSize);
|
|
WriteStorageArea(splitArea);
|
|
}
|
|
WriteStorageArea(storageArea, storageBytes);
|
|
|
|
if (documentAreas.ContainsKey(document.ID))
|
|
{
|
|
StorageArea oldStorageArea = documentAreas[document.ID];
|
|
WriteStorageArea(oldStorageArea);
|
|
storageAreas.Push(oldStorageArea);
|
|
}
|
|
documentAreas[document.ID] = storageArea;
|
|
}
|
|
}
|
|
|
|
public void Delete(Guid documentID)
|
|
{
|
|
lock (this)
|
|
{
|
|
if (documentAreas.ContainsKey(documentID))
|
|
{
|
|
StorageArea storageArea = documentAreas[documentID];
|
|
|
|
documentAreas.Remove(documentID);
|
|
storageArea = storageAreas.Push(storageArea);
|
|
WriteStorageArea(storageArea);
|
|
}
|
|
}
|
|
}
|
|
|
|
private StorageArea AppendStorageArea(int size)
|
|
{
|
|
CheckGranularity(ref size);
|
|
return new StorageArea(AppendOffset, size);
|
|
}
|
|
|
|
private void WriteStorageArea(StorageArea storageArea) => WriteStorageArea(storageArea, new byte[0]);
|
|
private void WriteStorageArea(StorageArea storageArea,byte[] data)
|
|
{
|
|
AssertOpen();
|
|
|
|
if (data.Length > (storageArea.Size - 4))
|
|
throw new ArgumentOutOfRangeException(nameof(data));
|
|
|
|
fileStream.Position = storageArea.Offset;
|
|
fileStream.WriteInteger(storageArea.Size);
|
|
fileStream.WriteBytes(data);
|
|
fileStream.WriteBytes(new byte[ storageArea.Size - 4 - data.Length]);
|
|
}
|
|
|
|
private void CheckGranularity(ref int i)
|
|
{
|
|
i = (i + GranularityMask) & ~GranularityMask;
|
|
}
|
|
|
|
|
|
|
|
}
|
|
}
|