FileCachePool.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. //------------------------------------------------------------------------------
  2. // 此代码版权(除特别声明或在XREF结尾的命名空间的代码)归作者本人若汝棋茗所有
  3. // 源代码使用协议遵循本仓库的开源协议及附加协议,若本仓库没有设置,则按MIT开源协议授权
  4. // CSDN博客:https://blog.csdn.net/qq_40374647
  5. // 哔哩哔哩视频:https://space.bilibili.com/94253567
  6. // Gitee源代码仓库:https://gitee.com/RRQM_Home
  7. // Github源代码仓库:https://github.com/RRQM
  8. // API首页:https://www.yuque.com/rrqm/touchsocket/index
  9. // 交流QQ群:234762506
  10. // 感谢您的下载和使用
  11. //------------------------------------------------------------------------------
  12. //------------------------------------------------------------------------------
  13. using System;
  14. using System.Collections.Generic;
  15. using System.IO;
  16. using System.Text;
  17. using System.Threading;
  18. using System.Web;
  19. using TouchSocket.Core;
  20. namespace TouchSocket.Http
  21. {
  22. /// <summary>
  23. /// 静态文件缓存池
  24. /// </summary>
  25. public class FileCachePool : DisposableObject
  26. {
  27. /// <summary>
  28. /// 添加委托
  29. /// </summary>
  30. /// <param name="cache"></param>
  31. /// <param name="key"></param>
  32. /// <param name="value"></param>
  33. /// <param name="timeout"></param>
  34. /// <returns></returns>
  35. public delegate bool InsertHandler(FileCachePool cache, string key, byte[] value, TimeSpan timeout);
  36. #region Cache items access
  37. /// <summary>
  38. /// Is the file cache empty?
  39. /// </summary>
  40. public bool Empty => entriesByKey.Count == 0;
  41. /// <summary>
  42. /// Get the file cache size
  43. /// </summary>
  44. public int Size => entriesByKey.Count;
  45. /// <summary>
  46. /// Add a new cache value with the given timeout into the file cache
  47. /// </summary>
  48. /// <param name="key">Key to add</param>
  49. /// <param name="value">Value to add</param>
  50. /// <param name="timeout">Cache timeout (default is 0 - no timeout)</param>
  51. /// <returns>'true' if the cache value was added, 'false' if the given key was not added</returns>
  52. public bool Add(string key, byte[] value, TimeSpan timeout = new TimeSpan())
  53. {
  54. using (new WriteLock(lockEx))
  55. {
  56. // Try to find and remove the previous key
  57. entriesByKey.Remove(key);
  58. // Update the cache entry
  59. entriesByKey.Add(key, new MemCacheEntry(value, timeout));
  60. return true;
  61. }
  62. }
  63. /// <summary>
  64. /// Try to find the cache value by the given key
  65. /// </summary>
  66. /// <param name="key">Key to find</param>
  67. /// <param name="data"></param>
  68. /// <returns>'true' and cache value if the cache value was found, 'false' if the given key was not found</returns>
  69. public bool Find(string key, out byte[] data)
  70. {
  71. using (new ReadLock(lockEx))
  72. {
  73. // Try to find the given key
  74. if (!entriesByKey.TryGetValue(key, out var cacheValue))
  75. {
  76. data = null;
  77. return false;
  78. }
  79. data = cacheValue.Value;
  80. return true;
  81. }
  82. }
  83. /// <summary>
  84. /// Remove the cache value with the given key from the file cache
  85. /// </summary>
  86. /// <param name="key">Key to remove</param>
  87. /// <returns>'true' if the cache value was removed, 'false' if the given key was not found</returns>
  88. public bool Remove(string key)
  89. {
  90. using (new WriteLock(lockEx))
  91. {
  92. return entriesByKey.Remove(key);
  93. }
  94. }
  95. #endregion Cache items access
  96. #region Cache management methods
  97. /// <summary>
  98. /// Insert a new cache path with the given timeout into the file cache
  99. /// </summary>
  100. /// <param name="path">Path to insert</param>
  101. /// <param name="prefix">Cache prefix (default is "/")</param>
  102. /// <param name="filter">Cache filter (default is "*.*")</param>
  103. /// <param name="timeout">Cache timeout (default is 0 - no timeout)</param>
  104. /// <param name="handler">Cache insert handler (default is 'return cache.Add(key, value, timeout)')</param>
  105. /// <returns>'true' if the cache path was setup, 'false' if failed to setup the cache path</returns>
  106. public bool InsertPath(string path, string prefix = "/", string filter = "*.*", TimeSpan timeout = new TimeSpan(), InsertHandler handler = null)
  107. {
  108. handler ??= (FileCachePool cache, string key, byte[] value, TimeSpan timespan) => cache.Add(key, value, timespan);
  109. // Try to find and remove the previous path
  110. RemovePathInternal(path);
  111. using (new WriteLock(lockEx))
  112. {
  113. // Add the given path to the cache
  114. pathsByKey.Add(path, new FileCacheEntry(this, prefix, path, filter, handler, timeout));
  115. // Create entries by path map
  116. entriesByPath[path] = new HashSet<string>();
  117. }
  118. // Insert the cache path
  119. if (!InsertPathInternal(path, path, prefix, timeout, handler))
  120. return false;
  121. return true;
  122. }
  123. /// <summary>
  124. /// Try to find the cache path
  125. /// </summary>
  126. /// <param name="path">Path to find</param>
  127. /// <returns>'true' if the cache path was found, 'false' if the given path was not found</returns>
  128. public bool FindPath(string path)
  129. {
  130. using (new ReadLock(lockEx))
  131. {
  132. // Try to find the given key
  133. return pathsByKey.ContainsKey(path);
  134. }
  135. }
  136. /// <summary>
  137. /// Remove the cache path from the file cache
  138. /// </summary>
  139. /// <param name="path">Path to remove</param>
  140. /// <returns>'true' if the cache path was removed, 'false' if the given path was not found</returns>
  141. public bool RemovePath(string path)
  142. {
  143. return RemovePathInternal(path);
  144. }
  145. /// <summary>
  146. /// Clear the memory cache
  147. /// </summary>
  148. public void Clear()
  149. {
  150. using (new WriteLock(lockEx))
  151. {
  152. // Stop all file system watchers
  153. foreach (var fileCacheEntry in pathsByKey)
  154. fileCacheEntry.Value.StopWatcher();
  155. // Clear all cache entries
  156. entriesByKey.Clear();
  157. entriesByPath.Clear();
  158. pathsByKey.Clear();
  159. }
  160. }
  161. #endregion Cache management methods
  162. #region Cache implementation
  163. private readonly ReaderWriterLockSlim lockEx = new ReaderWriterLockSlim();
  164. private readonly Dictionary<string, MemCacheEntry> entriesByKey = new Dictionary<string, MemCacheEntry>();
  165. private readonly Dictionary<string, HashSet<string>> entriesByPath = new Dictionary<string, HashSet<string>>();
  166. private readonly Dictionary<string, FileCacheEntry> pathsByKey = new Dictionary<string, FileCacheEntry>();
  167. private class MemCacheEntry
  168. {
  169. private readonly byte[] _value;
  170. private readonly TimeSpan _timespan;
  171. public byte[] Value => _value;
  172. public TimeSpan Timespan => _timespan;
  173. public MemCacheEntry(byte[] value, TimeSpan timespan = new TimeSpan())
  174. {
  175. _value = value;
  176. _timespan = timespan;
  177. }
  178. public MemCacheEntry(string value, TimeSpan timespan = new TimeSpan())
  179. {
  180. _value = Encoding.UTF8.GetBytes(value);
  181. _timespan = timespan;
  182. }
  183. };
  184. private class FileCacheEntry
  185. {
  186. private readonly string _prefix;
  187. private readonly string _path;
  188. private readonly InsertHandler _handler;
  189. private readonly TimeSpan _timespan;
  190. private readonly FileSystemWatcher _watcher;
  191. public FileCacheEntry(FileCachePool cache, string prefix, string path, string filter, InsertHandler handler, TimeSpan timespan)
  192. {
  193. _prefix = prefix;
  194. _path = path;
  195. _handler = handler;
  196. _timespan = timespan;
  197. _watcher = new FileSystemWatcher();
  198. // Start the filesystem watcher
  199. StartWatcher(cache, path, filter);
  200. }
  201. private void StartWatcher(FileCachePool cache, string path, string filter)
  202. {
  203. FileCacheEntry entry = this;
  204. // Initialize a new filesystem watcher
  205. _watcher.Created += (sender, e) => OnCreated(sender, e, cache, entry);
  206. _watcher.Changed += (sender, e) => OnChanged(sender, e, cache, entry);
  207. _watcher.Deleted += (sender, e) => OnDeleted(sender, e, cache, entry);
  208. _watcher.Renamed += (sender, e) => OnRenamed(sender, e, cache, entry);
  209. _watcher.Path = path;
  210. _watcher.IncludeSubdirectories = true;
  211. _watcher.Filter = filter;
  212. _watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
  213. _watcher.EnableRaisingEvents = true;
  214. }
  215. public void StopWatcher()
  216. {
  217. _watcher.Dispose();
  218. }
  219. private static bool IsDirectory(string path)
  220. {
  221. try
  222. {
  223. // Skip directory updates
  224. if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
  225. return true;
  226. }
  227. catch (Exception) { }
  228. return false;
  229. }
  230. private static void OnCreated(object sender, FileSystemEventArgs e, FileCachePool cache, FileCacheEntry entry)
  231. {
  232. var key = e.FullPath.Replace(entry._path, entry._prefix);
  233. var file = e.FullPath;
  234. // Skip missing files
  235. if (!File.Exists(file))
  236. return;
  237. // Skip directory updates
  238. if (IsDirectory(file))
  239. return;
  240. cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler);
  241. }
  242. private static void OnChanged(object sender, FileSystemEventArgs e, FileCachePool cache, FileCacheEntry entry)
  243. {
  244. if (e.ChangeType != WatcherChangeTypes.Changed)
  245. return;
  246. var key = e.FullPath.Replace(entry._path, entry._prefix);
  247. var file = e.FullPath;
  248. // Skip missing files
  249. if (!File.Exists(file))
  250. return;
  251. // Skip directory updates
  252. if (IsDirectory(file))
  253. return;
  254. cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler);
  255. }
  256. private static void OnDeleted(object sender, FileSystemEventArgs e, FileCachePool cache, FileCacheEntry entry)
  257. {
  258. var key = e.FullPath.Replace(entry._path, entry._prefix);
  259. var file = e.FullPath;
  260. cache.RemoveFileInternal(entry._path, key);
  261. }
  262. private static void OnRenamed(object sender, RenamedEventArgs e, FileCachePool cache, FileCacheEntry entry)
  263. {
  264. var oldKey = e.OldFullPath.Replace(entry._path, entry._prefix);
  265. var oldFile = e.OldFullPath;
  266. var newKey = e.FullPath.Replace(entry._path, entry._prefix);
  267. var newFile = e.FullPath;
  268. // Skip missing files
  269. if (!File.Exists(newFile))
  270. return;
  271. // Skip directory updates
  272. if (IsDirectory(newFile))
  273. return;
  274. cache.RemoveFileInternal(entry._path, oldKey);
  275. cache.InsertFileInternal(entry._path, newFile, newKey, entry._timespan, entry._handler);
  276. }
  277. };
  278. private bool InsertFileInternal(string path, string file, string key, TimeSpan timeout, InsertHandler handler)
  279. {
  280. try
  281. {
  282. key = key.Replace('\\', '/');
  283. file = file.Replace('\\', '/');
  284. // Load the cache file content
  285. var content = File.ReadAllBytes(file);
  286. if (!handler(this, key, content, timeout))
  287. return false;
  288. using (new WriteLock(lockEx))
  289. {
  290. // Update entries by path map
  291. entriesByPath[path].Add(key);
  292. }
  293. return true;
  294. }
  295. catch (Exception) { return false; }
  296. }
  297. private bool RemoveFileInternal(string path, string key)
  298. {
  299. try
  300. {
  301. key = key.Replace('\\', '/');
  302. using (new WriteLock(lockEx))
  303. {
  304. // Update entries by path map
  305. entriesByPath[path].Remove(key);
  306. }
  307. return Remove(key);
  308. }
  309. catch (Exception) { return false; }
  310. }
  311. private bool InsertPathInternal(string root, string path, string prefix, TimeSpan timeout, InsertHandler handler)
  312. {
  313. try
  314. {
  315. string keyPrefix = (string.IsNullOrEmpty(prefix) || (prefix == "/")) ? "/" : (prefix + "/");
  316. // Iterate through all directory entries
  317. foreach (var item in Directory.GetDirectories(path))
  318. {
  319. string key = keyPrefix /*+ HttpUtility.UrlDecode(Path.GetFileName(item))*/;
  320. // Recursively insert sub-directory
  321. if (!InsertPathInternal(root, item, key, timeout, handler))
  322. return false;
  323. }
  324. foreach (var item in Directory.GetFiles(path))
  325. {
  326. string key = keyPrefix /*+ HttpUtility.UrlDecode(Path.GetFileName(item))*/;
  327. // Insert file into the cache
  328. if (!InsertFileInternal(root, item, key, timeout, handler))
  329. return false;
  330. }
  331. return true;
  332. }
  333. catch (Exception) { return false; }
  334. }
  335. private bool RemovePathInternal(string path)
  336. {
  337. using (new WriteLock(lockEx))
  338. {
  339. // Try to find the given path
  340. if (!pathsByKey.TryGetValue(path, out var cacheValue))
  341. return false;
  342. // Stop the file system watcher
  343. cacheValue.StopWatcher();
  344. // Remove path entries
  345. foreach (var entryKey in entriesByPath[path])
  346. entriesByKey.Remove(entryKey);
  347. entriesByPath.Remove(path);
  348. // Remove cache path
  349. pathsByKey.Remove(path);
  350. return true;
  351. }
  352. }
  353. #endregion Cache implementation
  354. #region IDisposable implementation
  355. /// <summary>
  356. /// 释放
  357. /// </summary>
  358. /// <param name="disposing"></param>
  359. protected override void Dispose(bool disposing)
  360. {
  361. Clear();
  362. base.Dispose(disposing);
  363. }
  364. /// <summary>
  365. /// 析构函数
  366. /// </summary>
  367. ~FileCachePool()
  368. {
  369. // Simply call Dispose(false).
  370. Dispose(false);
  371. }
  372. #endregion IDisposable implementation
  373. }
  374. }