123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- using InABox.Clients;
- using System;
- using System.Collections.Concurrent;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Security.Cryptography;
- using System.Text;
- using System.Threading.Tasks;
- namespace InABox.Core
- {
- public interface ICachedDocument
- {
- Guid ID { get; }
- public void SerializeBinary(CoreBinaryWriter writer);
- void DeserializeBinary(CoreBinaryReader reader, bool full);
- }
- public class CachedDocument<T> : ISerializeBinary
- where T: ICachedDocument, new()
- {
- public Guid ID { get; set; }
- public DateTime CachedAt { get; set; }
- public T Document { get; set; }
- private CachedDocument()
- {
- }
- public CachedDocument(T document)
- {
- if (document.ID == Guid.Empty)
- {
- throw new Exception("Cannot cache document with no ID");
- }
- ID = document.ID;
- CachedAt = DateTime.Now;
- Document = document;
- }
- public void SerializeBinary(CoreBinaryWriter writer)
- {
- writer.Write(ID);
- writer.Write(CachedAt);
- Document.SerializeBinary(writer);
- }
- private void DeserializeBinary(CoreBinaryReader reader, bool full)
- {
- ID = reader.ReadGuid();
- CachedAt = reader.ReadDateTime();
- Document = new T();
- Document.DeserializeBinary(reader, full);
- }
- public void DeserializeBinary(CoreBinaryReader reader)
- {
- DeserializeBinary(reader, true);
- }
- public static CachedDocument<T> ReadHeader(Stream stream)
- {
- var cache = new CachedDocument<T>();
- cache.DeserializeBinary(new CoreBinaryReader(stream, BinarySerializationSettings.Latest), full: false);
- return cache;
- }
- public static CachedDocument<T> ReadFull(Stream stream)
- {
- var cache = new CachedDocument<T>();
- cache.DeserializeBinary(new CoreBinaryReader(stream, BinarySerializationSettings.Latest), full: true);
- return cache;
- }
- }
- public interface IDocumentCache
- {
- void Clear();
- void ClearOld();
- }
- /// <summary>
- /// A cache of documents that is saved in the AppData folder under a specific tag. Contrary to the name, this isn't just a <see cref="Document"/> cache, but
- /// in fact a way to cache any kind of object that implements <see cref="ICachedDocument"/>.
- /// </summary>
- /// <remarks>
- /// The files are stored with the name "<ID>.document", and they contain a binary serialised <see cref="CachedDocument"/>.
- /// This stores the date when the document was cached, allowing us to clear out old documents.
- /// </remarks>
- public abstract class DocumentCache<T> : IDocumentCache
- where T : class, ICachedDocument, new()
- {
- public string Tag { get; set; }
- public ConcurrentDictionary<Guid, byte> CachedDocuments { get; set; } = new ConcurrentDictionary<Guid, byte>();
- public ConcurrentDictionary<Guid, byte> UncachedDocuments { get; set; } = new ConcurrentDictionary<Guid, byte>();
- private bool _processing = false;
- private object _processingLock = new object();
- /// <summary>
- /// How long before documents are allowed to be cleaned.
- /// </summary>
- public abstract TimeSpan MaxAge { get; }
- public DocumentCache(string tag)
- {
- Tag = tag;
- EnsureCacheFolder();
- LoadCurrentCache();
- }
- #region Abstract Interface
- protected abstract T? LoadDocument(Guid id);
- #endregion
- #region Public Interface
- /// <summary>
- /// Check if the cache contains a document with the given <paramref name="id"/>.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- public bool Has(Guid id)
- {
- return CachedDocuments.ContainsKey(id);
- }
- /// <summary>
- /// Fetch a document from the cache or, if it hasn't been cached, through <see cref="LoadDocument(Guid)"/>.
- /// </summary>
- /// <param name="id"></param>
- /// <returns><see langword="null"/> if <see cref="LoadDocument(Guid)"/> returned <see langword="null"/>.</returns>
- public T? GetDocument(Guid id)
- {
- var file = GetFileName(id);
- if (File.Exists(file))
- {
- using var stream = File.OpenRead(file);
- var doc = CachedDocument<T>.ReadFull(stream);
- return doc.Document;
- }
- var document = LoadDocument(id);
- if(document != null)
- {
- Add(document);
- }
- return document;
- }
- /// <summary>
- /// Add a loaded document to the cache.
- /// </summary>
- /// <param name="document"></param>
- public void Add(T document)
- {
- var cached = new CachedDocument<T>(document);
- using (var file = File.Open(GetFileName(cached.ID), FileMode.Create))
- {
- cached.WriteBinary(file, BinarySerializationSettings.Latest);
- }
- CachedDocuments.TryAdd(cached.ID, 0);
- }
- /// <summary>
- /// Remove a document from the cache.
- /// </summary>
- /// <param name="id"></param>
- public void Remove(Guid id)
- {
- File.Delete(GetFileName(id));
- UncachedDocuments.TryRemove(id, out var _);
- CachedDocuments.TryRemove(id, out _);
- }
- /// <summary>
- /// Ensure that the cache contains <paramref name="documentIDs"/>. If it does not, a background worker will begin to download them.
- /// </summary>
- /// <param name="documentIDs"></param>
- public void Ensure(IEnumerable<Guid> documentIDs)
- {
- foreach(var docID in documentIDs)
- {
- if (docID != Guid.Empty)
- {
- if(!CachedDocuments.ContainsKey(docID))
- {
- UncachedDocuments.TryAdd(docID, 0);
- }
- }
- }
- CheckProcessing();
- }
- /// <summary>
- /// Like <see cref="Ensure(IEnumerable{Guid})"/>, but will clear out items that are not in <paramref name="documentIDs"/>.
- /// </summary>
- /// <param name="documentIDs"></param>
- public void EnsureStrict(IList<Guid> documentIDs)
- {
- foreach (var docID in documentIDs)
- {
- if (docID != Guid.Empty)
- {
- if (!CachedDocuments.ContainsKey(docID))
- {
- UncachedDocuments.TryAdd(docID, 0);
- }
- }
- }
- ClearWhere(x => !documentIDs.Contains(x));
- CheckProcessing();
- }
- /// <summary>
- /// Clear all old cached documents, according to <see cref="MaxAge"/>.
- /// </summary>
- public void ClearOld()
- {
- ClearWhere(docID =>
- {
- var filename = GetFileName(docID);
- if (File.Exists(filename))
- {
- using var stream = File.OpenRead(filename);
- var doc = CachedDocument<T>.ReadHeader(stream);
- return DateTime.Now - doc.CachedAt > MaxAge;
- }
- else
- {
- return true;
- }
- });
- }
- /// <summary>
- /// Clear the entire cache.
- /// </summary>
- public void Clear()
- {
- foreach (var file in Directory.EnumerateFiles(GetFolder()).Where(x => Path.GetExtension(x) == ".document"))
- {
- File.Delete(file);
- }
- CachedDocuments.Clear();
- UncachedDocuments.Clear();
- }
- #endregion
- #region Private Methods
- private void ClearWhere(Func<Guid, bool> predicate)
- {
- var toRemove = new List<Guid>();
- foreach (var docID in CachedDocuments.Keys)
- {
- if (predicate(docID))
- {
- File.Delete(GetFileName(docID));
- toRemove.Add(docID);
- }
- }
- foreach (var id in toRemove)
- {
- CachedDocuments.TryRemove(id, out var _);
- }
- }
- protected CachedDocument<T> GetHeader(Guid id)
- {
- var fileName = GetFileName(id);
- using var stream = File.OpenRead(fileName);
- return CachedDocument<T>.ReadHeader(stream);
- }
- protected CachedDocument<T> GetFull(Guid id)
- {
- var fileName = GetFileName(id);
- using var stream = File.OpenRead(fileName);
- return CachedDocument<T>.ReadFull(stream);
- }
- private void Process()
- {
- try
- {
- _processing = true;
- while (true)
- {
- Guid docID;
- lock (_processingLock)
- {
- docID = UncachedDocuments.Keys.FirstOrDefault();
- if (docID == Guid.Empty)
- {
- _processing = false;
- break;
- }
- }
- var document = LoadDocument(docID);
- if (document is null)
- {
- Logger.Send(LogType.Error, "", $"Document {docID} cannot be cached since it does not exist.");
- }
- else
- {
- Add(document);
- }
- UncachedDocuments.TryRemove(docID, out var _);
- }
- }
- catch(Exception ex)
- {
- CoreUtils.LogException("", ex);
- _processing = false;
- }
- }
-
- private void CheckProcessing()
- {
- lock (_processingLock)
- {
- if (!_processing && UncachedDocuments.Any())
- {
- Task.Run(Process);
- }
- }
- }
- private void EnsureCacheFolder()
- {
- Directory.CreateDirectory(GetFolder());
- }
- private void LoadCurrentCache()
- {
- foreach (var file in Directory.EnumerateFiles(GetFolder()).Where(x => Path.GetExtension(x) == ".document"))
- {
- try
- {
- using var stream = File.OpenRead(file);
- var doc = CachedDocument<T>.ReadHeader(stream);
- CachedDocuments.TryAdd(doc.ID, 0);
- }
- catch(Exception e)
- {
- CoreUtils.LogException("", e, "Error loading cache");
- // Skip;
- }
- }
- }
- private string GetFolder()
- {
- return Path.Combine(
- CoreUtils.GetPath(),
- ClientFactory.DatabaseID.ToString(),
- "_documentcache",
- Tag);
- }
- private string GetFileName(Guid documentID)
- {
- return Path.Combine(GetFolder(), $"{documentID}.document");
- }
- #endregion
- }
- public static class DocumentCaches
- {
- private static readonly Dictionary<Type, IDocumentCache> Caches = new Dictionary<Type, IDocumentCache>();
- #region Registry
- public static void RegisterAll()
- {
- var types = CoreUtils.TypeList(x => !x.IsAbstract && !x.IsGenericType && x.IsSubclassOf(typeof(IDocumentCache)));
- foreach(var type in types)
- {
- Caches.Add(type, (Activator.CreateInstance(type) as IDocumentCache)!);
- }
- }
- public static void RegisterCache<T>()
- where T : IDocumentCache, new()
- {
- Caches.Add(typeof(T), new T());
- }
- public static T GetOrRegister<T>()
- where T : class, IDocumentCache, new()
- {
- if(!Caches.TryGetValue(typeof(T), out var cache))
- {
- cache = new T();
- Caches.Add(typeof(T), new T());
- }
- return (cache as T)!;
- }
- #endregion
- #region Interface
- public static void Clear()
- {
- foreach(var cache in Caches.Values)
- {
- cache.Clear();
- }
- }
- public static void ClearOld()
- {
- foreach (var cache in Caches.Values)
- {
- cache.ClearOld();
- }
- }
- #endregion
- }
- /// <summary>
- /// An implementation of <see cref="ICachedDocument"/> for use with entities of type <see cref="Document"/>. The <see cref="Document.TimeStamp"/>
- /// is saved along with the document, allowing us to refresh updated documents.
- /// </summary>
- public class DocumentCachedDocument : ICachedDocument
- {
- public DateTime TimeStamp { get; set; }
- public Document? Document { get; set; }
- public Guid ID => Document?.ID ?? Guid.Empty;
- public DocumentCachedDocument() { }
- public DocumentCachedDocument(Document document)
- {
- Document = document;
- TimeStamp = document.TimeStamp;
- }
- public void DeserializeBinary(CoreBinaryReader reader, bool full)
- {
- TimeStamp = reader.ReadDateTime();
- if (full)
- {
- Document = reader.ReadObject<Document>();
- }
- }
- public void SerializeBinary(CoreBinaryWriter writer)
- {
- writer.Write(TimeStamp);
- if (Document is null)
- {
- throw new Exception("Cannot serialize incomplete CachedDocument");
- }
- writer.WriteObject(Document);
- }
- }
- /// <summary>
- /// Implements a <see cref="DocumentCache{T}"/> for use with <see cref="Document"/>.
- /// </summary>
- public abstract class DocumentCache : DocumentCache<DocumentCachedDocument>
- {
- public DocumentCache(string tag): base(tag) { }
- protected override DocumentCachedDocument? LoadDocument(Guid id)
- {
- var document = Client.Query(new Filter<Document>(x => x.ID).IsEqualTo(id))
- .ToObjects<Document>().FirstOrDefault();
- if(document != null)
- {
- return new DocumentCachedDocument(document);
- }
- else
- {
- return null;
- }
- }
-
- /// <summary>
- /// Fetch a bunch of documents from the cache or the database, optionally checking against the timestamp listed in the database.
- /// </summary>
- /// <param name="ids"></param>
- /// <param name="checkTimestamp">
- /// If <see langword="true"/>, then loads <see cref="Document.TimeStamp"/> from the database for all cached documents,
- /// and if they are older, updates the cache.
- /// </param>
- public IEnumerable<Document> LoadDocuments(IEnumerable<Guid> ids, bool checkTimestamp = false)
- {
- var cached = new List<Guid>();
- var toLoad = new List<Guid>();
- foreach (var docID in ids)
- {
- if (Has(docID))
- {
- cached.Add(docID);
- }
- else
- {
- toLoad.Add(docID);
- }
- }
- var loadedCached = new List<Document>();
- if (cached.Count > 0)
- {
- var docs = Client.Query(
- new Filter<Document>(x => x.ID).InList(cached.ToArray()),
- Columns.None<Document>().Add(x => x.TimeStamp, x => x.ID));
- foreach (var doc in docs.ToObjects<Document>())
- {
- try
- {
- var timestamp = GetHeader(doc.ID).Document.TimeStamp;
- if (doc.TimeStamp > timestamp)
- {
- toLoad.Add(doc.ID);
- }
- else
- {
- loadedCached.Add(GetFull(doc.ID).Document.Document!);
- }
- }
- catch (Exception e)
- {
- CoreUtils.LogException("", e, "Error loading cached file");
- toLoad.Add(doc.ID);
- }
- }
- }
- if (toLoad.Count > 0)
- {
- var loaded = Client.Query(new Filter<Document>(x => x.ID).InList(toLoad.ToArray()))
- .ToObjects<Document>().ToList();
- foreach (var loadedDoc in loaded)
- {
- Add(new DocumentCachedDocument(loadedDoc));
- }
- return loaded.Concat(loadedCached);
- }
- else
- {
- return loadedCached;
- }
- }
- }
- }
|