| 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;            }        }    }}
 |