Files
ichni_Official/Packages/com.tivadar.best.http/Runtime/Shared/Database/DiskManager.cs
2026-06-15 18:18:16 +08:00

296 lines
9.4 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Threading;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Shared.Databases
{
public sealed class DiskManagerOptions
{
// avg. size of certificates is 1390 (calculated from 2621 intermediate certificate)
public int MaxCacheSizeInBytes = 5 * 1024;
public string HashDigest = "SHA256";
}
public interface IDiskContentParser<T>
{
T Parse(Stream stream, int length);
void Encode(Stream stream, T content);
}
public sealed class DiskManager<T> : IDisposable
{
// TODO: store usage date/count and delete the oldest/least used?
struct CachePointer<CacheType>
{
public static readonly CachePointer<CacheType> Empty = new CachePointer<CacheType> { Position = -1, Length = -1, Content = default(CacheType) };
public int Position;
public int Length;
public CacheType Content;
public override string ToString()
{
return $"[CachePointer<{this.Content.GetType().Name}>({Position}, {Length}, {Content})]";
}
}
/// <summary>
/// Sum size of the cached contents
/// </summary>
public int CacheSize { get; private set; }
private Stream stream;
private List<CachePointer<T>> cache = new List<CachePointer<T>>();
private IDiskContentParser<T> diskContentParser;
private DiskManagerOptions options;
private FreeListManager freeListManager;
private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
public DiskManager(Stream stream, Stream freeListStream, IDiskContentParser<T> contentParser, DiskManagerOptions options)
{
if (!stream.CanSeek)
throw new ArgumentException("DiskManager - stream can't seek!");
this.stream = stream;
this.freeListManager = new FreeListManager(freeListStream);
this.diskContentParser = contentParser;
this.options = options;
}
public (int, int) Append(T content)
{
using (new WriteLock(this.rwLock))
{
int pos = -1;
int length = -1;
using (var buffer = new BufferPoolMemoryStream())
{
diskContentParser.Encode(buffer, content);
length = (int)buffer.Length;
var idx = this.freeListManager.FindFreeIndex(length);
if (idx >= 0)
pos = this.freeListManager.Occupy(idx, length);
else
pos = (int)this.stream.Length;
this.stream.Seek(pos, SeekOrigin.Begin);
buffer.Seek(0, SeekOrigin.Begin);
buffer.CopyTo(this.stream);
}
this.stream.Flush();
return (pos, length);
}
}
public void SaveChanged(Metadata metadata, T content)
{
using var _ = new WriteLock(this.rwLock);
int pos = -1;
int length = -1;
using (var buffer = new BufferPoolMemoryStream())
{
diskContentParser.Encode(buffer, content);
length = (int)buffer.Length;
this.freeListManager.Add(metadata.FilePosition, metadata.Length);
var idx = this.freeListManager.FindFreeIndex(length);
if (idx >= 0)
pos = this.freeListManager.Occupy(idx, length);
else
pos = (int)this.stream.Length;
this.stream.Seek(pos, SeekOrigin.Begin);
buffer.Seek(0, SeekOrigin.Begin);
buffer.CopyTo(this.stream);
}
this.stream.Flush();
metadata.FilePosition = pos;
metadata.Length = length;
}
public void Delete(Metadata metadata)
{
using (new WriteLock(this.rwLock))
{
this.freeListManager.Add(metadata.FilePosition, metadata.Length);
this.stream.Seek(metadata.FilePosition, SeekOrigin.Begin);
var buffer = BufferPool.Get(BufferPool.MIN_BUFFER_SIZE, true);
Array.Clear(buffer, 0, (int)BufferPool.MIN_BUFFER_SIZE);
int length = metadata.Length;
int iterationCount = length / (int)BufferPool.MIN_BUFFER_SIZE;
for (int i = 0; i < iterationCount; ++i)
{
this.stream.Write(buffer, 0, (int)BufferPool.MIN_BUFFER_SIZE);
length -= (int)BufferPool.MIN_BUFFER_SIZE;
}
this.stream.Write(buffer, 0, length);
this.stream.Flush();
BufferPool.Release(buffer);
}
}
public T Load(Metadata metadata)
{
using (new WriteLock(this.rwLock))
{
T parsedContent = default(T);
var cachePointer = GetCached(metadata.FilePosition);
if (cachePointer.Position != -1 && cachePointer.Content != null)
return cachePointer.Content;
this.stream.Seek(metadata.FilePosition, SeekOrigin.Begin);
parsedContent = diskContentParser.Parse(this.stream, metadata.Length);
AddToCache(parsedContent, metadata.FilePosition, metadata.Length);
return parsedContent;
}
}
public List<KeyValuePair<Meta, T>> LoadAll<Meta>(List<Meta> metadatas) where Meta : Metadata
{
using (new WriteLock(this.rwLock))
{
if (metadatas == null || metadatas.Count == 0)
return null;
metadatas.Sort((m1, m2) => m1.FilePosition.CompareTo(m2.FilePosition));
List<KeyValuePair<Meta, T>> result = new List<KeyValuePair<Meta, T>>(metadatas.Count);
for (int i = 0; i < metadatas.Count; ++i)
{
var metadata = metadatas[i];
result.Add(new KeyValuePair<Meta, T>(metadata, Load(metadata)));
}
return result;
}
}
public void Clear()
{
using (new WriteLock(this.rwLock))
{
this.freeListManager.Clear();
this.stream.SetLength(0);
this.stream.Flush();
this.cache.Clear();
this.CacheSize = 0;
}
}
private CachePointer<T> GetCached(int position)
{
for (int i = 0; i < this.cache.Count; ++i)
{
var cache = this.cache[i];
if (cache.Position == position)
return cache;
}
return CachePointer<T>.Empty;
}
private void AddToCache(T parsedContent, int pos, int length)
{
if (this.options.MaxCacheSizeInBytes >= length)
{
this.cache.Insert(0, new CachePointer<T>
{
Position = pos,
Length = length,
Content = parsedContent
});
this.CacheSize += length;
}
while (this.CacheSize > this.options.MaxCacheSizeInBytes && this.cache.Count > 0)
{
var removingCache = this.cache[this.cache.Count - 1];
this.cache.RemoveAt(this.cache.Count - 1);
this.CacheSize -= removingCache.Length;
}
}
public BufferSegment CalculateHash()
{
using (new WriteLock(this.rwLock))
{
this.stream.Seek(0, SeekOrigin.Begin);
#if UNITY_WEBGL && !UNITY_EDITOR
var hash = System.Security.Cryptography.HashAlgorithm.Create(this.options.HashDigest);
var result = hash.ComputeHash(this.stream);
return new BufferSegment(result, 0, result.Length);
#elif !BESTHTTP_DISABLE_ALTERNATE_SSL
var digest = Best.HTTP.SecureProtocol.Org.BouncyCastle.Security.DigestUtilities.GetDigest(this.options.HashDigest);
byte[] buffer = BufferPool.Get(4 * 1024, true);
int readCount = 0;
while ((readCount = this.stream.Read(buffer, 0, buffer.Length)) > 0)
{
digest.BlockUpdate(buffer, 0, readCount);
}
BufferPool.Release(buffer);
byte[] result = BufferPool.Get(digest.GetDigestSize(), true);
int length = digest.DoFinal(result, 0);
return new BufferSegment(result, 0, length);
#else
throw new NotImplementedException(nameof(CalculateHash));
#endif
}
}
public void Save()
{
this.stream.Flush();
this.freeListManager.Save();
}
public void Dispose()
{
this.freeListManager.Dispose();
this.stream.Flush();
this.stream.Close();
this.stream = null;
this.rwLock.Dispose();
GC.SuppressFinalize(this);
}
}
}