DocumentCache.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. using InABox.Clients;
  2. using System;
  3. using System.Collections.Concurrent;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Security.Cryptography;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. namespace InABox.Core
  11. {
  12. public interface ICachedDocument
  13. {
  14. Guid ID { get; }
  15. public void SerializeBinary(CoreBinaryWriter writer);
  16. void DeserializeBinary(CoreBinaryReader reader, bool full);
  17. }
  18. public class CachedDocument<T> : ISerializeBinary
  19. where T: ICachedDocument, new()
  20. {
  21. public Guid ID { get; set; }
  22. public DateTime CachedAt { get; set; }
  23. public T Document { get; set; }
  24. private CachedDocument()
  25. {
  26. }
  27. public CachedDocument(T document)
  28. {
  29. if (document.ID == Guid.Empty)
  30. {
  31. throw new Exception("Cannot cache document with no ID");
  32. }
  33. ID = document.ID;
  34. CachedAt = DateTime.Now;
  35. Document = document;
  36. }
  37. public void SerializeBinary(CoreBinaryWriter writer)
  38. {
  39. writer.Write(ID);
  40. writer.Write(CachedAt);
  41. Document.SerializeBinary(writer);
  42. }
  43. private void DeserializeBinary(CoreBinaryReader reader, bool full)
  44. {
  45. ID = reader.ReadGuid();
  46. CachedAt = reader.ReadDateTime();
  47. Document = new T();
  48. Document.DeserializeBinary(reader, full);
  49. }
  50. public void DeserializeBinary(CoreBinaryReader reader)
  51. {
  52. DeserializeBinary(reader, true);
  53. }
  54. public static CachedDocument<T> ReadHeader(Stream stream)
  55. {
  56. var cache = new CachedDocument<T>();
  57. cache.DeserializeBinary(new CoreBinaryReader(stream, BinarySerializationSettings.Latest), full: false);
  58. return cache;
  59. }
  60. public static CachedDocument<T> ReadFull(Stream stream)
  61. {
  62. var cache = new CachedDocument<T>();
  63. cache.DeserializeBinary(new CoreBinaryReader(stream, BinarySerializationSettings.Latest), full: true);
  64. return cache;
  65. }
  66. }
  67. public interface IDocumentCache
  68. {
  69. void Clear();
  70. void ClearOld();
  71. }
  72. /// <summary>
  73. /// A cache of documents that is saved in the AppData folder under a specific tag.
  74. /// </summary>
  75. /// <remarks>
  76. /// The files are stored with the name "&lt;ID&gt;.document", and they contain a binary serialised <see cref="CachedDocument"/>.
  77. /// This stores the date when the document was cached, allowing us to clear out old documents.
  78. /// </remarks>
  79. public abstract class DocumentCache<T> : IDocumentCache
  80. where T : class, ICachedDocument, new()
  81. {
  82. public string Tag { get; set; }
  83. public ConcurrentDictionary<Guid, byte> CachedDocuments { get; set; } = new ConcurrentDictionary<Guid, byte>();
  84. public ConcurrentDictionary<Guid, byte> UncachedDocuments { get; set; } = new ConcurrentDictionary<Guid, byte>();
  85. private bool _processing = false;
  86. private object _processingLock = new object();
  87. /// <summary>
  88. /// How long before documents are allowed to be cleaned.
  89. /// </summary>
  90. public abstract TimeSpan MaxAge { get; }
  91. public DocumentCache(string tag)
  92. {
  93. Tag = tag;
  94. EnsureCacheFolder();
  95. LoadCurrentCache();
  96. }
  97. #region Abstract Interface
  98. protected abstract T? LoadDocument(Guid id);
  99. #endregion
  100. #region Public Interface
  101. /// <summary>
  102. /// Check if the cache contains a document with the given <paramref name="id"/>.
  103. /// </summary>
  104. /// <param name="id"></param>
  105. /// <returns></returns>
  106. public bool Has(Guid id)
  107. {
  108. return CachedDocuments.ContainsKey(id);
  109. }
  110. /// <summary>
  111. /// Fetch a document from the cache or, if it hasn't been cached, through <see cref="LoadDocument(Guid)"/>.
  112. /// </summary>
  113. /// <param name="id"></param>
  114. /// <returns><see langword="null"/> if <see cref="LoadDocument(Guid)"/> returned <see langword="null"/>.</returns>
  115. public T? GetDocument(Guid id)
  116. {
  117. var file = GetFileName(id);
  118. if (File.Exists(file))
  119. {
  120. using var stream = File.OpenRead(file);
  121. var doc = CachedDocument<T>.ReadFull(stream);
  122. return doc.Document;
  123. }
  124. var document = LoadDocument(id);
  125. if(document != null)
  126. {
  127. Add(document);
  128. }
  129. return document;
  130. }
  131. /// <summary>
  132. /// Add a loaded document to the cache.
  133. /// </summary>
  134. /// <param name="document"></param>
  135. public void Add(T document)
  136. {
  137. var cached = new CachedDocument<T>(document);
  138. using (var file = File.Open(GetFileName(cached.ID), FileMode.Create))
  139. {
  140. cached.WriteBinary(file, BinarySerializationSettings.Latest);
  141. }
  142. CachedDocuments.TryAdd(cached.ID, 0);
  143. }
  144. /// <summary>
  145. /// Remove a document from the cache.
  146. /// </summary>
  147. /// <param name="id"></param>
  148. public void Remove(Guid id)
  149. {
  150. File.Delete(GetFileName(id));
  151. UncachedDocuments.TryRemove(id, out var _);
  152. CachedDocuments.TryRemove(id, out _);
  153. }
  154. /// <summary>
  155. /// Ensure that the cache contains <paramref name="documentIDs"/>. If it does not, a background worker will begin to download them.
  156. /// </summary>
  157. /// <param name="documentIDs"></param>
  158. public void Ensure(IEnumerable<Guid> documentIDs)
  159. {
  160. foreach(var docID in documentIDs)
  161. {
  162. if (docID != Guid.Empty)
  163. {
  164. if(!CachedDocuments.ContainsKey(docID))
  165. {
  166. UncachedDocuments.TryAdd(docID, 0);
  167. }
  168. }
  169. }
  170. CheckProcessing();
  171. }
  172. /// <summary>
  173. /// Like <see cref="Ensure(IEnumerable{Guid})"/>, but will clear out items that are not in <paramref name="documentIDs"/>.
  174. /// </summary>
  175. /// <param name="documentIDs"></param>
  176. public void EnsureStrict(IList<Guid> documentIDs)
  177. {
  178. foreach (var docID in documentIDs)
  179. {
  180. if (docID != Guid.Empty)
  181. {
  182. if (!CachedDocuments.ContainsKey(docID))
  183. {
  184. UncachedDocuments.TryAdd(docID, 0);
  185. }
  186. }
  187. }
  188. ClearWhere(x => !documentIDs.Contains(x));
  189. CheckProcessing();
  190. }
  191. /// <summary>
  192. /// Clear all old cached documents, according to <see cref="MaxAge"/>.
  193. /// </summary>
  194. public void ClearOld()
  195. {
  196. ClearWhere(docID =>
  197. {
  198. var filename = GetFileName(docID);
  199. if (File.Exists(filename))
  200. {
  201. using var stream = File.OpenRead(filename);
  202. var doc = CachedDocument<T>.ReadHeader(stream);
  203. return DateTime.Now - doc.CachedAt > MaxAge;
  204. }
  205. else
  206. {
  207. return true;
  208. }
  209. });
  210. }
  211. /// <summary>
  212. /// Clear the entire cache.
  213. /// </summary>
  214. public void Clear()
  215. {
  216. foreach (var file in Directory.EnumerateFiles(GetFolder()).Where(x => Path.GetExtension(x) == ".document"))
  217. {
  218. File.Delete(file);
  219. }
  220. CachedDocuments.Clear();
  221. UncachedDocuments.Clear();
  222. }
  223. #endregion
  224. #region Private Methods
  225. private void ClearWhere(Func<Guid, bool> predicate)
  226. {
  227. var toRemove = new List<Guid>();
  228. foreach (var docID in CachedDocuments.Keys)
  229. {
  230. if (predicate(docID))
  231. {
  232. File.Delete(GetFileName(docID));
  233. toRemove.Add(docID);
  234. }
  235. }
  236. foreach (var id in toRemove)
  237. {
  238. CachedDocuments.TryRemove(id, out var _);
  239. }
  240. }
  241. protected CachedDocument<T> GetHeader(Guid id)
  242. {
  243. var fileName = GetFileName(id);
  244. using var stream = File.OpenRead(fileName);
  245. return CachedDocument<T>.ReadHeader(stream);
  246. }
  247. protected CachedDocument<T> GetFull(Guid id)
  248. {
  249. var fileName = GetFileName(id);
  250. using var stream = File.OpenRead(fileName);
  251. return CachedDocument<T>.ReadFull(stream);
  252. }
  253. private void Process()
  254. {
  255. try
  256. {
  257. _processing = true;
  258. while (true)
  259. {
  260. Guid docID;
  261. lock (_processingLock)
  262. {
  263. docID = UncachedDocuments.Keys.FirstOrDefault();
  264. if (docID == Guid.Empty)
  265. {
  266. _processing = false;
  267. break;
  268. }
  269. }
  270. var document = LoadDocument(docID);
  271. if (document is null)
  272. {
  273. Logger.Send(LogType.Error, "", $"Document {docID} cannot be cached since it does not exist.");
  274. }
  275. else
  276. {
  277. Add(document);
  278. }
  279. UncachedDocuments.TryRemove(docID, out var _);
  280. }
  281. }
  282. catch(Exception ex)
  283. {
  284. CoreUtils.LogException("", ex);
  285. _processing = false;
  286. }
  287. }
  288. private void CheckProcessing()
  289. {
  290. lock (_processingLock)
  291. {
  292. if (!_processing && UncachedDocuments.Any())
  293. {
  294. Task.Run(Process);
  295. }
  296. }
  297. }
  298. private void EnsureCacheFolder()
  299. {
  300. Directory.CreateDirectory(GetFolder());
  301. }
  302. private void LoadCurrentCache()
  303. {
  304. foreach (var file in Directory.EnumerateFiles(GetFolder()).Where(x => Path.GetExtension(x) == ".document"))
  305. {
  306. try
  307. {
  308. using var stream = File.OpenRead(file);
  309. var doc = CachedDocument<T>.ReadHeader(stream);
  310. CachedDocuments.TryAdd(doc.ID, 0);
  311. }
  312. catch(Exception e)
  313. {
  314. CoreUtils.LogException("", e, "Error loading cache");
  315. // Skip;
  316. }
  317. }
  318. }
  319. private string GetFolder()
  320. {
  321. return Path.Combine(
  322. CoreUtils.GetPath(),
  323. ClientFactory.DatabaseID.ToString(),
  324. "_documentcache",
  325. Tag);
  326. }
  327. private string GetFileName(Guid documentID)
  328. {
  329. return Path.Combine(GetFolder(), $"{documentID}.document");
  330. }
  331. #endregion
  332. }
  333. public static class DocumentCaches
  334. {
  335. private static readonly Dictionary<Type, IDocumentCache> Caches = new Dictionary<Type, IDocumentCache>();
  336. #region Registry
  337. public static void RegisterAll()
  338. {
  339. var types = CoreUtils.TypeList(x => !x.IsAbstract && !x.IsGenericType && x.IsSubclassOf(typeof(IDocumentCache)));
  340. foreach(var type in types)
  341. {
  342. Caches.Add(type, (Activator.CreateInstance(type) as IDocumentCache)!);
  343. }
  344. }
  345. public static void RegisterCache<T>()
  346. where T : IDocumentCache, new()
  347. {
  348. Caches.Add(typeof(T), new T());
  349. }
  350. public static T GetOrRegister<T>()
  351. where T : class, IDocumentCache, new()
  352. {
  353. if(!Caches.TryGetValue(typeof(T), out var cache))
  354. {
  355. cache = new T();
  356. Caches.Add(typeof(T), new T());
  357. }
  358. return (cache as T)!;
  359. }
  360. #endregion
  361. #region Interface
  362. public static void Clear()
  363. {
  364. foreach(var cache in Caches.Values)
  365. {
  366. cache.Clear();
  367. }
  368. }
  369. public static void ClearOld()
  370. {
  371. foreach (var cache in Caches.Values)
  372. {
  373. cache.ClearOld();
  374. }
  375. }
  376. #endregion
  377. }
  378. }