Added a CachedByteArray class that will store compressed (disk-ready) images on the appropriate media
241 lines
9.3 KiB
C#
241 lines
9.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace RBG_Server
|
|
{
|
|
/// <summary>
|
|
/// Stores data in either memory or disk, depending on the required behaviour of the system, namely, if the
|
|
/// byte array exceeds the size of the Memory Limit (objects can be required to stay in RAM)
|
|
/// </summary>
|
|
public struct CachedByteArray : IDisposable
|
|
{
|
|
// Static event, so that every instance of CachedByteArray is affected by a change in memory limit
|
|
private static event EventHandler MemoryLimitModifiedEvent;
|
|
// Properties to keep track of the actual allocated cache amounts
|
|
private static long memoryAllocationTotal = 0;
|
|
private static long memoryAllocationRAM = 0;
|
|
// static fields
|
|
private static int memoryLimit = 1024 * 1024 * 1; // Start with a memory limit of 1 MB
|
|
|
|
private static readonly string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Remote Board Game\\Cache Objects");
|
|
// static properties
|
|
public static long MemoryAllocationRAM { get { return memoryAllocationRAM; } }
|
|
public static long MemoryAllocationTotal { get { return memoryAllocationTotal; } }
|
|
public static long MemoryAllocationDisk { get { return memoryAllocationTotal - memoryAllocationRAM; } }
|
|
|
|
private SemaphoreSlim accessSem;
|
|
|
|
public bool IsInRam { get; private set; }
|
|
|
|
// struct data
|
|
private bool useOnlyRAM;
|
|
|
|
private Stream dataStream;
|
|
|
|
private string dataHashCode;
|
|
public bool UseOnlyRAM { get => useOnlyRAM; set => useOnlyRAM = value; }
|
|
// Properties from the array itself
|
|
public long Length => dataStream.Length;
|
|
/// <summary>
|
|
/// Set the memory limit for keeping data in RAM, as opposed to storing on disk.
|
|
/// Will raise events if modified, so all instances will be refactored to disk if appropriate
|
|
/// </summary>
|
|
public static int MemoryLimit
|
|
{
|
|
get
|
|
{
|
|
return memoryLimit;
|
|
}
|
|
set
|
|
{
|
|
memoryLimit = value;
|
|
MemoryLimitModifiedEvent?.Invoke(null, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
public CachedByteArray(byte[] b) : this(b, false)
|
|
{
|
|
}
|
|
|
|
public CachedByteArray(byte[] b, bool useOnlyRAM)
|
|
{
|
|
accessSem = new SemaphoreSlim(0,1);
|
|
this.useOnlyRAM = useOnlyRAM;
|
|
dataHashCode = GenerateDataHash(b);
|
|
if (!useOnlyRAM && b.Length > MemoryLimit)
|
|
{
|
|
// Save to a file; load the data
|
|
string path = Path.Combine(folderPath, dataHashCode);
|
|
dataStream = CopyIntoFile(b, path);
|
|
IsInRam = false;
|
|
}
|
|
else
|
|
{
|
|
dataStream = new MemoryStream(b);
|
|
memoryAllocationRAM += dataStream.Length;
|
|
IsInRam = true;
|
|
}
|
|
dataStream.Position = 0;
|
|
memoryAllocationTotal += dataStream.Length;
|
|
// Signal the semaphore is ready
|
|
accessSem.Release();
|
|
// Listen to changes in the memory limit
|
|
MemoryLimitModifiedEvent += CachedByteArray_memoryLimitModifiedEvent;
|
|
}
|
|
/// <summary>
|
|
/// Generates a SHA256 hash of the data in the array, and returns it in a safe string format
|
|
/// </summary>
|
|
/// <param name="byteData"></param>
|
|
/// <returns></returns>
|
|
private static string GenerateDataHash(byte[] byteData)
|
|
{
|
|
using SHA256 dataHash = SHA256.Create();
|
|
byte[] hashCode = dataHash.ComputeHash(byteData);
|
|
string generatedHashCode = "";
|
|
foreach (byte hashByte in hashCode)
|
|
{
|
|
generatedHashCode += $"{hashByte:X2}";
|
|
}
|
|
return generatedHashCode;
|
|
}
|
|
/// <summary>
|
|
/// Copies the given <see cref="byte[]"/> into the file at <paramref name="filePath"/>, opening the stream, and returning it
|
|
/// </summary>
|
|
/// <param name="byteData"></param>
|
|
/// <param name="filePath"></param>
|
|
/// <returns></returns>
|
|
private static Stream CopyIntoFile(byte[] byteData, string filePath)
|
|
{
|
|
FileStream s = File.OpenWrite(filePath);
|
|
foreach (byte b in byteData)
|
|
{
|
|
s.WriteByte(b);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
private void CachedByteArray_memoryLimitModifiedEvent(object sender, EventArgs e)
|
|
{
|
|
if (!useOnlyRAM)
|
|
{
|
|
if (dataStream.Length > MemoryLimit && IsInRam)
|
|
{
|
|
// Memory stream exceeds the limit; create an FS and copy
|
|
accessSem.Wait();
|
|
// Get the underlying byte[]
|
|
byte[] dataBytes = ((MemoryStream)dataStream).ToArray();
|
|
// Generate the hashcode from the data
|
|
dataHashCode = GenerateDataHash(dataBytes);
|
|
// Create the file, and copy the data
|
|
dataStream = CopyIntoFile(dataBytes, Path.Combine(folderPath, dataHashCode));
|
|
// Disregard the allocation from RAM
|
|
memoryAllocationRAM -= dataStream.Length;
|
|
accessSem.Release();
|
|
}
|
|
else if (dataStream.Length <= MemoryLimit && !IsInRam)
|
|
{
|
|
// Data stream is no larger than the limit, but the file is saved on disk; load to RAM instead
|
|
// Obtain a lock on the stream
|
|
accessSem.Wait();
|
|
// Copy data to memory
|
|
dataStream.Position = 0;
|
|
MemoryStream ms = new();
|
|
dataStream.CopyTo(ms);
|
|
// Increase the amount noted in RAM
|
|
memoryAllocationRAM += ms.Length;
|
|
// Close the FileStream
|
|
dataStream.Close();
|
|
// Delete the file
|
|
DeleteFile(Path.Combine(folderPath, dataHashCode));
|
|
// Set the datastream to the memory location
|
|
dataStream = ms;
|
|
accessSem.Release();
|
|
// Check the loaded data matches the hashcode we already have
|
|
string loadedHashCode = GenerateDataHash(ms.ToArray());
|
|
if (loadedHashCode != dataHashCode)
|
|
{
|
|
throw new UnauthorizedAccessException("Data was modified");
|
|
}
|
|
}
|
|
// Otherwise, the data is already in the correct spot, so we don't need to do anything else
|
|
}
|
|
}
|
|
private static void DeleteFile(string filePath)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(filePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine(ex);
|
|
}
|
|
}
|
|
public static implicit operator byte[](CachedByteArray c)
|
|
{
|
|
byte[] dataBytes = new byte[c.dataStream.Length];
|
|
c.accessSem.Wait();
|
|
if (c.IsInRam)
|
|
{
|
|
// We can use the underlying array from the MemoryStream
|
|
dataBytes = (c.dataStream as MemoryStream).ToArray();
|
|
}
|
|
else
|
|
{
|
|
// Ensure we read from the beginning of the stream
|
|
c.dataStream.Position = 0;
|
|
c.dataStream.Read(dataBytes, 0, dataBytes.Length);
|
|
}
|
|
c.accessSem.Release();
|
|
return dataBytes;
|
|
}
|
|
|
|
public static explicit operator CachedByteArray(byte[] b) => new CachedByteArray(b);
|
|
/// <summary>
|
|
/// Array index operator, allows the CachedByteArray to act more completely as a byte[]
|
|
/// </summary>
|
|
/// <param name="index"></param>
|
|
/// <returns></returns>
|
|
public byte this[int index]
|
|
{
|
|
get
|
|
{
|
|
int b;
|
|
// Aquire the semaphore, so that the read is effectively atomic (ensures the read is performed on the expected data)
|
|
accessSem.Wait();
|
|
dataStream.Position = index;
|
|
b = dataStream.ReadByte();
|
|
accessSem.Release();
|
|
if (b == -1)
|
|
{
|
|
throw new IndexOutOfRangeException(index.ToString());
|
|
}
|
|
return (byte)b;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
accessSem.Wait();
|
|
// Remove the event listener
|
|
MemoryLimitModifiedEvent -= CachedByteArray_memoryLimitModifiedEvent;
|
|
// Remove the allocation from the counter
|
|
memoryAllocationTotal -= dataStream.Length;
|
|
memoryAllocationRAM -= dataStream.Length;
|
|
// If the FileStream is opened, close and dispose it
|
|
dataStream?.Dispose();
|
|
// If the file is on disk, remove it
|
|
DeleteFile(Path.Combine(folderPath, dataHashCode));
|
|
// Release all resources obtained by the semaphore
|
|
accessSem.Dispose();
|
|
}
|
|
}
|
|
}
|