FileCache.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. using System.Threading;
  6. using System.Web;
  7. namespace NetCoreServer
  8. {
  9. /// <summary>
  10. /// File cache is used to cache files in memory with optional timeouts.
  11. /// FileSystemWatcher is used to monitor file system changes in cached
  12. /// directories.
  13. /// </summary>
  14. /// <remarks>Thread-safe.</remarks>
  15. public class FileCache : IDisposable
  16. {
  17. public delegate bool InsertHandler(FileCache cache, string key, byte[] value, TimeSpan timeout);
  18. #region Cache items access
  19. /// <summary>
  20. /// Is the file cache empty?
  21. /// </summary>
  22. public bool Empty => _entriesByKey.Count == 0;
  23. /// <summary>
  24. /// Get the file cache size
  25. /// </summary>
  26. public int Size => _entriesByKey.Count;
  27. /// <summary>
  28. /// Add a new cache value with the given timeout into the file cache
  29. /// </summary>
  30. /// <param name="key">Key to add</param>
  31. /// <param name="value">Value to add</param>
  32. /// <param name="timeout">Cache timeout (default is 0 - no timeout)</param>
  33. /// <returns>'true' if the cache value was added, 'false' if the given key was not added</returns>
  34. public bool Add(string key, byte[] value, TimeSpan timeout = new TimeSpan())
  35. {
  36. using (new WriteLock(_lockEx))
  37. {
  38. // Try to find and remove the previous key
  39. _entriesByKey.Remove(key);
  40. // Update the cache entry
  41. _entriesByKey.Add(key, new MemCacheEntry(value, timeout));
  42. return true;
  43. }
  44. }
  45. /// <summary>
  46. /// Try to find the cache value by the given key
  47. /// </summary>
  48. /// <param name="key">Key to find</param>
  49. /// <returns>'true' and cache value if the cache value was found, 'false' if the given key was not found</returns>
  50. public (bool, byte[]) Find(string key)
  51. {
  52. using (new ReadLock(_lockEx))
  53. {
  54. // Try to find the given key
  55. if (!_entriesByKey.TryGetValue(key, out var cacheValue))
  56. return (false, new byte[0]);
  57. return (true, cacheValue.Value);
  58. }
  59. }
  60. /// <summary>
  61. /// Remove the cache value with the given key from the file cache
  62. /// </summary>
  63. /// <param name="key">Key to remove</param>
  64. /// <returns>'true' if the cache value was removed, 'false' if the given key was not found</returns>
  65. public bool Remove(string key)
  66. {
  67. using (new WriteLock(_lockEx))
  68. {
  69. return _entriesByKey.Remove(key);
  70. }
  71. }
  72. #endregion
  73. #region Cache management methods
  74. /// <summary>
  75. /// Insert a new cache path with the given timeout into the file cache
  76. /// </summary>
  77. /// <param name="path">Path to insert</param>
  78. /// <param name="prefix">Cache prefix (default is "/")</param>
  79. /// <param name="filter">Cache filter (default is "*.*")</param>
  80. /// <param name="timeout">Cache timeout (default is 0 - no timeout)</param>
  81. /// <param name="handler">Cache insert handler (default is 'return cache.Add(key, value, timeout)')</param>
  82. /// <returns>'true' if the cache path was setup, 'false' if failed to setup the cache path</returns>
  83. public bool InsertPath(string path, string prefix = "/", string filter = "*.*", TimeSpan timeout = new TimeSpan(), InsertHandler handler = null)
  84. {
  85. handler ??= (FileCache cache, string key, byte[] value, TimeSpan timespan) => cache.Add(key, value, timespan);
  86. // Try to find and remove the previous path
  87. RemovePathInternal(path);
  88. using (new WriteLock(_lockEx))
  89. {
  90. // Add the given path to the cache
  91. _pathsByKey.Add(path, new FileCacheEntry(this, prefix, path, filter, handler, timeout));
  92. // Create entries by path map
  93. _entriesByPath[path] = new HashSet<string>();
  94. }
  95. // Insert the cache path
  96. if (!InsertPathInternal(path, path, prefix, timeout, handler))
  97. return false;
  98. return true;
  99. }
  100. /// <summary>
  101. /// Try to find the cache path
  102. /// </summary>
  103. /// <param name="path">Path to find</param>
  104. /// <returns>'true' if the cache path was found, 'false' if the given path was not found</returns>
  105. public bool FindPath(string path)
  106. {
  107. using (new ReadLock(_lockEx))
  108. {
  109. // Try to find the given key
  110. return _pathsByKey.ContainsKey(path);
  111. }
  112. }
  113. /// <summary>
  114. /// Remove the cache path from the file cache
  115. /// </summary>
  116. /// <param name="path">Path to remove</param>
  117. /// <returns>'true' if the cache path was removed, 'false' if the given path was not found</returns>
  118. public bool RemovePath(string path)
  119. {
  120. return RemovePathInternal(path);
  121. }
  122. /// <summary>
  123. /// Clear the memory cache
  124. /// </summary>
  125. public void Clear()
  126. {
  127. using (new WriteLock(_lockEx))
  128. {
  129. // Stop all file system watchers
  130. foreach (var fileCacheEntry in _pathsByKey)
  131. fileCacheEntry.Value.StopWatcher();
  132. // Clear all cache entries
  133. _entriesByKey.Clear();
  134. _entriesByPath.Clear();
  135. _pathsByKey.Clear();
  136. }
  137. }
  138. #endregion
  139. #region Cache implementation
  140. private readonly ReaderWriterLockSlim _lockEx = new ReaderWriterLockSlim();
  141. private Dictionary<string, MemCacheEntry> _entriesByKey = new Dictionary<string, MemCacheEntry>();
  142. private Dictionary<string, HashSet<string>> _entriesByPath = new Dictionary<string, HashSet<string>>();
  143. private Dictionary<string, FileCacheEntry> _pathsByKey = new Dictionary<string, FileCacheEntry>();
  144. private class MemCacheEntry
  145. {
  146. private readonly byte[] _value;
  147. private readonly TimeSpan _timespan;
  148. public byte[] Value => _value;
  149. public TimeSpan Timespan => _timespan;
  150. public MemCacheEntry(byte[] value, TimeSpan timespan = new TimeSpan())
  151. {
  152. _value = value;
  153. _timespan = timespan;
  154. }
  155. public MemCacheEntry(string value, TimeSpan timespan = new TimeSpan())
  156. {
  157. _value = Encoding.UTF8.GetBytes(value);
  158. _timespan = timespan;
  159. }
  160. };
  161. private class FileCacheEntry
  162. {
  163. private readonly string _prefix;
  164. private readonly string _path;
  165. private readonly InsertHandler _handler;
  166. private readonly TimeSpan _timespan;
  167. private readonly FileSystemWatcher _watcher;
  168. public FileCacheEntry(FileCache cache, string prefix, string path, string filter, InsertHandler handler, TimeSpan timespan)
  169. {
  170. _prefix = prefix.Replace('\\', '/').RemoveSuffix('/');
  171. _path = path.Replace('\\', '/').RemoveSuffix('/');
  172. _handler = handler;
  173. _timespan = timespan;
  174. _watcher = new FileSystemWatcher();
  175. // Start the filesystem watcher
  176. StartWatcher(cache, path, filter);
  177. }
  178. private void StartWatcher(FileCache cache, string path, string filter)
  179. {
  180. FileCacheEntry entry = this;
  181. // Initialize a new filesystem watcher
  182. _watcher.Created += (sender, e) => OnCreated(sender, e, cache, entry);
  183. _watcher.Changed += (sender, e) => OnChanged(sender, e, cache, entry);
  184. _watcher.Deleted += (sender, e) => OnDeleted(sender, e, cache, entry);
  185. _watcher.Renamed += (sender, e) => OnRenamed(sender, e, cache, entry);
  186. _watcher.Path = path;
  187. _watcher.IncludeSubdirectories = true;
  188. _watcher.Filter = filter;
  189. _watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
  190. _watcher.EnableRaisingEvents = true;
  191. }
  192. public void StopWatcher()
  193. {
  194. _watcher.Dispose();
  195. }
  196. private static bool IsDirectory(string path)
  197. {
  198. try
  199. {
  200. // Skip directory updates
  201. if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
  202. return true;
  203. }
  204. catch (Exception) {}
  205. return false;
  206. }
  207. private static void OnCreated(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry)
  208. {
  209. var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/');
  210. var file = e.FullPath.Replace('\\', '/').RemoveSuffix('/');
  211. // Skip missing files
  212. if (!File.Exists(file))
  213. return;
  214. // Skip directory updates
  215. if (IsDirectory(file))
  216. return;
  217. cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler);
  218. }
  219. private static void OnChanged(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry)
  220. {
  221. if (e.ChangeType != WatcherChangeTypes.Changed)
  222. return;
  223. var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/');
  224. var file = e.FullPath.Replace('\\', '/').RemoveSuffix('/');
  225. // Skip missing files
  226. if (!File.Exists(file))
  227. return;
  228. // Skip directory updates
  229. if (IsDirectory(file))
  230. return;
  231. cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler);
  232. }
  233. private static void OnDeleted(object sender, FileSystemEventArgs e, FileCache cache, FileCacheEntry entry)
  234. {
  235. var key = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/');
  236. var file = e.FullPath.Replace('\\', '/').RemoveSuffix('/');
  237. cache.RemoveFileInternal(entry._path, key);
  238. }
  239. private static void OnRenamed(object sender, RenamedEventArgs e, FileCache cache, FileCacheEntry entry)
  240. {
  241. var oldKey = e.OldFullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/');
  242. var oldFile = e.OldFullPath.Replace('\\', '/').RemoveSuffix('/');
  243. var newKey = e.FullPath.Replace('\\', '/').Replace(entry._path, entry._prefix).RemoveSuffix('/');
  244. var newFile = e.FullPath.Replace('\\', '/').RemoveSuffix('/');
  245. // Skip missing files
  246. if (!File.Exists(newFile))
  247. return;
  248. // Skip directory updates
  249. if (IsDirectory(newFile))
  250. return;
  251. cache.RemoveFileInternal(entry._path, oldKey);
  252. cache.InsertFileInternal(entry._path, newFile, newKey, entry._timespan, entry._handler);
  253. }
  254. };
  255. private bool InsertFileInternal(string path, string file, string key, TimeSpan timeout, InsertHandler handler)
  256. {
  257. try
  258. {
  259. // Load the cache file content
  260. var content = File.ReadAllBytes(file);
  261. if (!handler(this, key, content, timeout))
  262. return false;
  263. using (new WriteLock(_lockEx))
  264. {
  265. // Update entries by path map
  266. _entriesByPath[path].Add(key);
  267. }
  268. return true;
  269. }
  270. catch (Exception) { return false; }
  271. }
  272. private bool RemoveFileInternal(string path, string key)
  273. {
  274. try
  275. {
  276. using (new WriteLock(_lockEx))
  277. {
  278. // Update entries by path map
  279. _entriesByPath[path].Remove(key);
  280. }
  281. return Remove(key);
  282. }
  283. catch (Exception) { return false; }
  284. }
  285. private bool InsertPathInternal(string root, string path, string prefix, TimeSpan timeout, InsertHandler handler)
  286. {
  287. try
  288. {
  289. // Iterate through all directory entries
  290. foreach (var item in Directory.GetDirectories(path))
  291. {
  292. string key = prefix + "/" + HttpUtility.UrlDecode(Path.GetFileName(item));
  293. // Recursively insert sub-directory
  294. if (!InsertPathInternal(root, item, key, timeout, handler))
  295. return false;
  296. }
  297. foreach (var item in Directory.GetFiles(path))
  298. {
  299. string key = prefix + "/" + HttpUtility.UrlDecode(Path.GetFileName(item));
  300. // Insert file into the cache
  301. if (!InsertFileInternal(root, item, key, timeout, handler))
  302. return false;
  303. }
  304. return true;
  305. }
  306. catch (Exception) { return false; }
  307. }
  308. private bool RemovePathInternal(string path)
  309. {
  310. using (new WriteLock(_lockEx))
  311. {
  312. // Try to find the given path
  313. if (!_pathsByKey.TryGetValue(path, out var cacheValue))
  314. return false;
  315. // Stop the file system watcher
  316. cacheValue.StopWatcher();
  317. // Remove path entries
  318. foreach (var entryKey in _entriesByPath[path])
  319. _entriesByKey.Remove(entryKey);
  320. _entriesByPath.Remove(path);
  321. // Remove cache path
  322. _pathsByKey.Remove(path);
  323. return true;
  324. }
  325. }
  326. #endregion
  327. #region IDisposable implementation
  328. // Disposed flag.
  329. private bool _disposed;
  330. // Implement IDisposable.
  331. public void Dispose()
  332. {
  333. Dispose(true);
  334. GC.SuppressFinalize(this);
  335. }
  336. protected virtual void Dispose(bool disposingManagedResources)
  337. {
  338. // The idea here is that Dispose(Boolean) knows whether it is
  339. // being called to do explicit cleanup (the Boolean is true)
  340. // versus being called due to a garbage collection (the Boolean
  341. // is false). This distinction is useful because, when being
  342. // disposed explicitly, the Dispose(Boolean) method can safely
  343. // execute code using reference type fields that refer to other
  344. // objects knowing for sure that these other objects have not been
  345. // finalized or disposed of yet. When the Boolean is false,
  346. // the Dispose(Boolean) method should not execute code that
  347. // refer to reference type fields because those objects may
  348. // have already been finalized."
  349. if (!_disposed)
  350. {
  351. if (disposingManagedResources)
  352. {
  353. // Dispose managed resources here...
  354. Clear();
  355. }
  356. // Dispose unmanaged resources here...
  357. // Set large fields to null here...
  358. // Mark as disposed.
  359. _disposed = true;
  360. }
  361. }
  362. #endregion
  363. }
  364. /// <summary>
  365. /// Disposable lock class performs exit action on dispose operation.
  366. /// </summary>
  367. public class DisposableLock : IDisposable
  368. {
  369. private readonly Action _exitLock;
  370. public DisposableLock(Action exitLock)
  371. {
  372. _exitLock = exitLock;
  373. }
  374. public void Dispose()
  375. {
  376. _exitLock();
  377. }
  378. }
  379. /// <summary>
  380. /// Read lock class enters read lock on construction and performs exit read lock on dispose.
  381. /// </summary>
  382. public class ReadLock : DisposableLock
  383. {
  384. public ReadLock(ReaderWriterLockSlim locker) : base(locker.ExitReadLock)
  385. {
  386. locker.EnterReadLock();
  387. }
  388. }
  389. /// <summary>
  390. /// Write lock class enters write lock on construction and performs exit write lock on dispose.
  391. /// </summary>
  392. public class WriteLock : DisposableLock
  393. {
  394. public WriteLock(ReaderWriterLockSlim locker) : base(locker.ExitWriteLock)
  395. {
  396. locker.EnterWriteLock();
  397. }
  398. }
  399. /// <summary>
  400. /// String extensions utility class.
  401. /// </summary>
  402. public static class StringExtensions
  403. {
  404. public static string RemoveSuffix(this string str, char toRemove) => str.EndsWith(toRemove) ? str.Substring(0, str.Length - 1) : str;
  405. public static string RemoveSuffix(this string str, string toRemove) => str.EndsWith(toRemove) ? str.Substring(0, str.Length - toRemove.Length) : str;
  406. }
  407. }