HTTPCacheService.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. #if !BESTHTTP_DISABLE_CACHING
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Threading;
  5. //
  6. // Version 1: Initial release
  7. // Version 2: Filenames are generated from an index.
  8. //
  9. namespace BestHTTP.Caching
  10. {
  11. using BestHTTP.Core;
  12. using BestHTTP.Extensions;
  13. using BestHTTP.PlatformSupport.FileSystem;
  14. using BestHTTP.PlatformSupport.Threading;
  15. public sealed class UriComparer : IEqualityComparer<Uri>
  16. {
  17. public bool Equals(Uri x, Uri y)
  18. {
  19. return Uri.Compare(x, y, UriComponents.HttpRequestUrl, UriFormat.SafeUnescaped, StringComparison.Ordinal) == 0;
  20. }
  21. public int GetHashCode(Uri uri)
  22. {
  23. return uri.ToString().GetHashCode();
  24. }
  25. }
  26. public static class HTTPCacheService
  27. {
  28. #region Properties & Fields
  29. /// <summary>
  30. /// Library file-format versioning support
  31. /// </summary>
  32. private const int LibraryVersion = 3;
  33. public static bool IsSupported
  34. {
  35. get
  36. {
  37. if (IsSupportCheckDone)
  38. return isSupported;
  39. try
  40. {
  41. #if UNITY_WEBGL && !UNITY_EDITOR
  42. // Explicitly disable cahing under WebGL
  43. isSupported = false;
  44. #else
  45. // If DirectoryExists throws an exception we will set IsSupprted to false
  46. HTTPManager.IOService.DirectoryExists(HTTPManager.GetRootCacheFolder());
  47. isSupported = true;
  48. #endif
  49. }
  50. catch
  51. {
  52. isSupported = false;
  53. HTTPManager.Logger.Warning("HTTPCacheService", "Cache Service Disabled!");
  54. }
  55. finally
  56. {
  57. IsSupportCheckDone = true;
  58. }
  59. return isSupported;
  60. }
  61. }
  62. private static bool isSupported;
  63. private static bool IsSupportCheckDone;
  64. private static Dictionary<Uri, HTTPCacheFileInfo> library;
  65. private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
  66. private static Dictionary<UInt64, HTTPCacheFileInfo> UsedIndexes = new Dictionary<ulong, HTTPCacheFileInfo>();
  67. internal static string CacheFolder { get; private set; }
  68. private static string LibraryPath { get; set; }
  69. private volatile static bool InClearThread;
  70. private volatile static bool InMaintainenceThread;
  71. /// <summary>
  72. /// This property returns true while the service is in a Clear or Maintenance thread.
  73. /// </summary>
  74. public static bool IsDoingMaintainence { get { return InClearThread || InMaintainenceThread; } }
  75. /// <summary>
  76. /// Stores the index of the next stored entity. The entity's file name is generated from this index.
  77. /// </summary>
  78. private static UInt64 NextNameIDX;
  79. #endregion
  80. static HTTPCacheService()
  81. {
  82. NextNameIDX = 0x0001;
  83. }
  84. #region Common Functions
  85. internal static void CheckSetup()
  86. {
  87. if (!HTTPCacheService.IsSupported)
  88. return;
  89. try
  90. {
  91. SetupCacheFolder();
  92. LoadLibrary();
  93. }
  94. catch
  95. { }
  96. }
  97. internal static void SetupCacheFolder()
  98. {
  99. if (!HTTPCacheService.IsSupported)
  100. return;
  101. try
  102. {
  103. if (string.IsNullOrEmpty(CacheFolder) || string.IsNullOrEmpty(LibraryPath))
  104. {
  105. CacheFolder = System.IO.Path.Combine(HTTPManager.GetRootCacheFolder(), "HTTPCache");
  106. if (!HTTPManager.IOService.DirectoryExists(CacheFolder))
  107. HTTPManager.IOService.DirectoryCreate(CacheFolder);
  108. LibraryPath = System.IO.Path.Combine(HTTPManager.GetRootCacheFolder(), "Library");
  109. }
  110. }
  111. catch
  112. {
  113. isSupported = false;
  114. HTTPManager.Logger.Warning("HTTPCacheService", "Cache Service Disabled!");
  115. }
  116. }
  117. internal static UInt64 GetNameIdx()
  118. {
  119. UInt64 result = NextNameIDX;
  120. do
  121. {
  122. NextNameIDX = ++NextNameIDX % UInt64.MaxValue;
  123. } while (UsedIndexes.ContainsKey(NextNameIDX));
  124. return result;
  125. }
  126. public static bool HasEntity(Uri uri)
  127. {
  128. if (!IsSupported)
  129. return false;
  130. CheckSetup();
  131. using (new ReadLock(rwLock))
  132. return library.ContainsKey(uri);
  133. }
  134. public static bool DeleteEntity(Uri uri, bool removeFromLibrary = true)
  135. {
  136. if (!IsSupported)
  137. return false;
  138. // 2019.05.10: Removed all locking except the one on the library.
  139. CheckSetup();
  140. using (new WriteLock(rwLock))
  141. {
  142. DeleteEntityImpl(uri, removeFromLibrary, false);
  143. return true;
  144. }
  145. }
  146. private static void DeleteEntityImpl(Uri uri, bool removeFromLibrary = true, bool useLocking = false)
  147. {
  148. HTTPCacheFileInfo info;
  149. bool inStats = library.TryGetValue(uri, out info);
  150. if (inStats)
  151. info.Delete();
  152. if (inStats && removeFromLibrary)
  153. {
  154. if (useLocking)
  155. rwLock.EnterWriteLock();
  156. try
  157. {
  158. library.Remove(uri);
  159. UsedIndexes.Remove(info.MappedNameIDX);
  160. }
  161. finally
  162. {
  163. if (useLocking)
  164. rwLock.ExitWriteLock();
  165. }
  166. }
  167. PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.SaveCacheLibrary));
  168. }
  169. internal static bool IsCachedEntityExpiresInTheFuture(HTTPRequest request)
  170. {
  171. if (!IsSupported || request.DisableCache)
  172. return false;
  173. CheckSetup();
  174. HTTPCacheFileInfo info = null;
  175. using (new ReadLock(rwLock))
  176. {
  177. if (!library.TryGetValue(request.CurrentUri, out info))
  178. return false;
  179. }
  180. return info.WillExpireInTheFuture(request.State == HTTPRequestStates.ConnectionTimedOut ||
  181. request.State == HTTPRequestStates.TimedOut ||
  182. request.State == HTTPRequestStates.Error ||
  183. (request.State == HTTPRequestStates.Finished && request.Response != null && request.Response.StatusCode >= 500));
  184. }
  185. /// <summary>
  186. /// Utility function to set the cache control headers according to the spec.: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
  187. /// </summary>
  188. /// <param name="request"></param>
  189. internal static void SetHeaders(HTTPRequest request)
  190. {
  191. if (!IsSupported)
  192. return;
  193. CheckSetup();
  194. request.RemoveHeader("If-None-Match");
  195. request.RemoveHeader("If-Modified-Since");
  196. HTTPCacheFileInfo info = null;
  197. using (new ReadLock(rwLock))
  198. {
  199. if (!library.TryGetValue(request.CurrentUri, out info))
  200. return;
  201. }
  202. info.SetUpRevalidationHeaders(request);
  203. }
  204. #endregion
  205. #region Get Functions
  206. public static HTTPCacheFileInfo GetEntity(Uri uri)
  207. {
  208. if (!IsSupported)
  209. return null;
  210. CheckSetup();
  211. HTTPCacheFileInfo info = null;
  212. using (new ReadLock(rwLock))
  213. library.TryGetValue(uri, out info);
  214. return info;
  215. }
  216. internal static HTTPResponse GetFullResponse(HTTPRequest request)
  217. {
  218. if (!IsSupported)
  219. return null;
  220. CheckSetup();
  221. HTTPCacheFileInfo info = null;
  222. using (new ReadLock(rwLock))
  223. {
  224. if (!library.TryGetValue(request.CurrentUri, out info))
  225. return null;
  226. return info.ReadResponseTo(request);
  227. }
  228. }
  229. #endregion
  230. #region Storing
  231. /// <summary>
  232. /// Checks if the given response can be cached. http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
  233. /// </summary>
  234. /// <returns>Returns true if cacheable, false otherwise.</returns>
  235. internal static bool IsCacheble(Uri uri, HTTPMethods method, HTTPResponse response)
  236. {
  237. if (!IsSupported)
  238. return false;
  239. if (method != HTTPMethods.Get)
  240. return false;
  241. if (response == null)
  242. return false;
  243. // https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.12 - Cache Replacement
  244. // It MAY insert it into cache storage and MAY, if it meets all other requirements, use it to respond to any future requests that would previously have caused the old response to be returned.
  245. //if (response.StatusCode == 304)
  246. // return false;
  247. // Partial response
  248. if (response.StatusCode == 206)
  249. return false;
  250. if (response.StatusCode < 200 || response.StatusCode >= 400)
  251. return false;
  252. //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
  253. bool hasValidMaxAge = false;
  254. var cacheControls = response.GetHeaderValues("cache-control");
  255. if (cacheControls != null)
  256. {
  257. if (cacheControls.Exists(headerValue =>
  258. {
  259. HeaderParser parser = new HeaderParser(headerValue);
  260. if (parser.Values != null && parser.Values.Count > 0) {
  261. for (int i = 0; i < parser.Values.Count; ++i)
  262. {
  263. var value = parser.Values[i];
  264. // https://csswizardry.com/2019/03/cache-control-for-civilians/#no-store
  265. if (value.Key == "no-store")
  266. return true;
  267. if (value.Key == "max-age" && value.HasValue)
  268. {
  269. double maxAge;
  270. if (double.TryParse(value.Value, out maxAge))
  271. {
  272. // A negative max-age value is a no cache
  273. if (maxAge <= 0)
  274. return true;
  275. hasValidMaxAge = true;
  276. }
  277. }
  278. }
  279. }
  280. return false;
  281. }))
  282. return false;
  283. }
  284. var pragmas = response.GetHeaderValues("pragma");
  285. if (pragmas != null)
  286. {
  287. if (pragmas.Exists(headerValue =>
  288. {
  289. string value = headerValue.ToLower();
  290. return value.Contains("no-store") || value.Contains("no-cache");
  291. }))
  292. return false;
  293. }
  294. // Responses with byte ranges not supported yet.
  295. var byteRanges = response.GetHeaderValues("content-range");
  296. if (byteRanges != null)
  297. return false;
  298. // Store only if at least one caching header with proper value present
  299. var etag = response.GetFirstHeaderValue("ETag");
  300. if (!string.IsNullOrEmpty(etag))
  301. return true;
  302. var expires = response.GetFirstHeaderValue("Expires").ToDateTime(DateTime.FromBinary(0));
  303. if (expires >= DateTime.UtcNow)
  304. return true;
  305. if (response.GetFirstHeaderValue("Last-Modified") != null)
  306. return true;
  307. return hasValidMaxAge;
  308. }
  309. internal static HTTPCacheFileInfo Store(Uri uri, HTTPMethods method, HTTPResponse response)
  310. {
  311. if (response == null || response.Data == null || response.Data.Length == 0)
  312. return null;
  313. if (!IsSupported)
  314. return null;
  315. CheckSetup();
  316. HTTPCacheFileInfo info = null;
  317. using (new WriteLock(rwLock))
  318. {
  319. if (!library.TryGetValue(uri, out info))
  320. {
  321. library.Add(uri, info = new HTTPCacheFileInfo(uri));
  322. UsedIndexes.Add(info.MappedNameIDX, info);
  323. }
  324. try
  325. {
  326. info.Store(response);
  327. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  328. HTTPManager.Logger.Verbose("HTTPCacheService", string.Format("{0} - Saved to cache", uri.ToString()), response.baseRequest.Context);
  329. }
  330. catch
  331. {
  332. // If something happens while we write out the response, than we will delete it because it might be in an invalid state.
  333. DeleteEntityImpl(uri);
  334. throw;
  335. }
  336. }
  337. return info;
  338. }
  339. internal static void SetUpCachingValues(Uri uri, HTTPResponse response)
  340. {
  341. if (!IsSupported)
  342. return;
  343. CheckSetup();
  344. using (new WriteLock(rwLock))
  345. {
  346. HTTPCacheFileInfo info = null;
  347. if (!library.TryGetValue(uri, out info))
  348. {
  349. library.Add(uri, info = new HTTPCacheFileInfo(uri));
  350. UsedIndexes.Add(info.MappedNameIDX, info);
  351. }
  352. try
  353. {
  354. info.SetUpCachingValues(response);
  355. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  356. HTTPManager.Logger.Verbose("HTTPCacheService", string.Format("{0} - SetUpCachingValues done!", uri.ToString()), response.baseRequest.Context);
  357. }
  358. catch
  359. {
  360. // If something happens while we write out the response, than we will delete it because it might be in an invalid state.
  361. DeleteEntityImpl(uri);
  362. throw;
  363. }
  364. }
  365. }
  366. internal static System.IO.Stream PrepareStreamed(Uri uri, HTTPResponse response)
  367. {
  368. if (!IsSupported)
  369. return null;
  370. CheckSetup();
  371. HTTPCacheFileInfo info;
  372. using (new WriteLock(rwLock))
  373. {
  374. if (!library.TryGetValue(uri, out info))
  375. {
  376. library.Add(uri, info = new HTTPCacheFileInfo(uri));
  377. UsedIndexes.Add(info.MappedNameIDX, info);
  378. }
  379. }
  380. try
  381. {
  382. return info.GetSaveStream(response);
  383. }
  384. catch
  385. {
  386. // If something happens while we write out the response, than we will delete it because it might be in an invalid state.
  387. DeleteEntityImpl(uri, true, true);
  388. throw;
  389. }
  390. }
  391. #endregion
  392. #region Public Maintenance Functions
  393. /// <summary>
  394. /// Deletes all cache entity. Non blocking.
  395. /// <remarks>Call it only if there no requests currently processed, because cache entries can be deleted while a server sends back a 304 result, so there will be no data to read from the cache!</remarks>
  396. /// </summary>
  397. public static void BeginClear()
  398. {
  399. if (!IsSupported)
  400. return;
  401. if (InClearThread)
  402. return;
  403. InClearThread = true;
  404. SetupCacheFolder();
  405. PlatformSupport.Threading.ThreadedRunner.RunShortLiving(ClearImpl);
  406. }
  407. private static void ClearImpl()
  408. {
  409. if (!IsSupported)
  410. return;
  411. CheckSetup();
  412. using (new WriteLock(rwLock))
  413. {
  414. try
  415. {
  416. // GetFiles will return a string array that contains the files in the folder with the full path
  417. string[] cacheEntries = HTTPManager.IOService.GetFiles(CacheFolder);
  418. if (cacheEntries != null)
  419. for (int i = 0; i < cacheEntries.Length; ++i)
  420. {
  421. // We need a try-catch block because between the Directory.GetFiles call and the File.Delete calls a maintenance job, or other file operations can delete any file from the cache folder.
  422. // So while there might be some problem with any file, we don't want to abort the whole for loop
  423. try
  424. {
  425. HTTPManager.IOService.FileDelete(cacheEntries[i]);
  426. }
  427. catch
  428. { }
  429. }
  430. }
  431. finally
  432. {
  433. UsedIndexes.Clear();
  434. library.Clear();
  435. NextNameIDX = 0x0001;
  436. InClearThread = false;
  437. PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.SaveCacheLibrary));
  438. }
  439. }
  440. }
  441. /// <summary>
  442. /// Deletes all expired cache entity.
  443. /// <remarks>Call it only if there no requests currently processed, because cache entries can be deleted while a server sends back a 304 result, so there will be no data to read from the cache!</remarks>
  444. /// </summary>
  445. public static void BeginMaintainence(HTTPCacheMaintananceParams maintananceParam)
  446. {
  447. if (maintananceParam == null)
  448. throw new ArgumentNullException("maintananceParams == null");
  449. if (!HTTPCacheService.IsSupported)
  450. return;
  451. if (InMaintainenceThread)
  452. return;
  453. InMaintainenceThread = true;
  454. SetupCacheFolder();
  455. PlatformSupport.Threading.ThreadedRunner.RunShortLiving(MaintananceImpl, maintananceParam);
  456. }
  457. private static void MaintananceImpl(HTTPCacheMaintananceParams maintananceParam)
  458. {
  459. CheckSetup();
  460. using (new WriteLock(rwLock))
  461. {
  462. try
  463. {
  464. // Delete cache entries older than the given time.
  465. DateTime deleteOlderAccessed = DateTime.UtcNow - maintananceParam.DeleteOlder;
  466. List<HTTPCacheFileInfo> removedEntities = new List<HTTPCacheFileInfo>();
  467. foreach (var kvp in library)
  468. if (kvp.Value.LastAccess < deleteOlderAccessed)
  469. {
  470. DeleteEntityImpl(kvp.Key, false, false);
  471. removedEntities.Add(kvp.Value);
  472. }
  473. for (int i = 0; i < removedEntities.Count; ++i)
  474. {
  475. library.Remove(removedEntities[i].Uri);
  476. UsedIndexes.Remove(removedEntities[i].MappedNameIDX);
  477. }
  478. removedEntities.Clear();
  479. ulong cacheSize = GetCacheSizeImpl();
  480. // This step will delete all entries starting with the oldest LastAccess property while the cache size greater then the MaxCacheSize in the given param.
  481. if (cacheSize > maintananceParam.MaxCacheSize)
  482. {
  483. List<HTTPCacheFileInfo> fileInfos = new List<HTTPCacheFileInfo>(library.Count);
  484. foreach (var kvp in library)
  485. fileInfos.Add(kvp.Value);
  486. fileInfos.Sort();
  487. int idx = 0;
  488. while (cacheSize >= maintananceParam.MaxCacheSize && idx < fileInfos.Count)
  489. {
  490. try
  491. {
  492. var fi = fileInfos[idx];
  493. ulong length = (ulong)fi.BodyLength;
  494. DeleteEntityImpl(fi.Uri);
  495. cacheSize -= length;
  496. }
  497. catch
  498. { }
  499. finally
  500. {
  501. ++idx;
  502. }
  503. }
  504. }
  505. }
  506. finally
  507. {
  508. InMaintainenceThread = false;
  509. PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.SaveCacheLibrary));
  510. }
  511. }
  512. }
  513. public static int GetCacheEntityCount()
  514. {
  515. if (!HTTPCacheService.IsSupported)
  516. return 0;
  517. CheckSetup();
  518. using (new ReadLock(rwLock))
  519. {
  520. return library.Count;
  521. }
  522. }
  523. public static ulong GetCacheSize()
  524. {
  525. if (!IsSupported)
  526. return 0;
  527. CheckSetup();
  528. using (new ReadLock (rwLock))
  529. {
  530. return GetCacheSizeImpl();
  531. }
  532. }
  533. private static ulong GetCacheSizeImpl()
  534. {
  535. ulong size = 0;
  536. foreach (var kvp in library)
  537. if (kvp.Value.BodyLength > 0)
  538. size += (ulong)kvp.Value.BodyLength;
  539. return size;
  540. }
  541. #endregion
  542. #region Cache Library Management
  543. private static void LoadLibrary()
  544. {
  545. // Already loaded?
  546. if (library != null)
  547. return;
  548. if (!IsSupported)
  549. return;
  550. int version = 1;
  551. using (new WriteLock(rwLock))
  552. {
  553. library = new Dictionary<Uri, HTTPCacheFileInfo>(new UriComparer());
  554. try
  555. {
  556. using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.OpenRead))
  557. using (var br = new System.IO.BinaryReader(fs))
  558. {
  559. version = br.ReadInt32();
  560. if (version > 1)
  561. NextNameIDX = br.ReadUInt64();
  562. int statCount = br.ReadInt32();
  563. for (int i = 0; i < statCount; ++i)
  564. {
  565. Uri uri = new Uri(br.ReadString());
  566. var entity = new HTTPCacheFileInfo(uri, br, version);
  567. if (entity.IsExists())
  568. {
  569. library.Add(uri, entity);
  570. if (version > 1)
  571. UsedIndexes.Add(entity.MappedNameIDX, entity);
  572. }
  573. }
  574. }
  575. }
  576. catch (Exception ex)
  577. {
  578. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  579. HTTPManager.Logger.Exception("HTTPCacheService", "LoadLibrary", ex);
  580. }
  581. }
  582. if (version == 1)
  583. BeginClear();
  584. else
  585. DeleteUnusedFiles();
  586. }
  587. internal static void SaveLibrary()
  588. {
  589. if (library == null)
  590. return;
  591. if (!IsSupported)
  592. return;
  593. using (new WriteLock(rwLock))
  594. {
  595. try
  596. {
  597. using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.Create))
  598. using (var bw = new System.IO.BinaryWriter(fs))
  599. {
  600. bw.Write(LibraryVersion);
  601. bw.Write(NextNameIDX);
  602. bw.Write(library.Count);
  603. foreach (var kvp in library)
  604. {
  605. bw.Write(kvp.Key.ToString());
  606. kvp.Value.SaveTo(bw);
  607. }
  608. }
  609. }
  610. catch (Exception ex)
  611. {
  612. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  613. HTTPManager.Logger.Exception("HTTPCacheService", "SaveLibrary", ex);
  614. }
  615. }
  616. }
  617. internal static void SetBodyLength(Uri uri, int bodyLength)
  618. {
  619. if (!IsSupported)
  620. return;
  621. CheckSetup();
  622. using (new WriteLock(rwLock))
  623. {
  624. HTTPCacheFileInfo fileInfo;
  625. if (library.TryGetValue(uri, out fileInfo))
  626. fileInfo.BodyLength = bodyLength;
  627. else
  628. {
  629. library.Add(uri, fileInfo = new HTTPCacheFileInfo(uri, DateTime.UtcNow, bodyLength));
  630. UsedIndexes.Add(fileInfo.MappedNameIDX, fileInfo);
  631. }
  632. }
  633. }
  634. /// <summary>
  635. /// Deletes all files from the cache folder that isn't in the Library.
  636. /// </summary>
  637. private static void DeleteUnusedFiles()
  638. {
  639. if (!IsSupported)
  640. return;
  641. CheckSetup();
  642. // GetFiles will return a string array that contains the files in the folder with the full path
  643. string[] cacheEntries = HTTPManager.IOService.GetFiles(CacheFolder);
  644. for (int i = 0; i < cacheEntries.Length; ++i)
  645. {
  646. // We need a try-catch block because between the Directory.GetFiles call and the File.Delete calls a maintenance job, or other file operations can delete any file from the cache folder.
  647. // So while there might be some problem with any file, we don't want to abort the whole for loop
  648. try
  649. {
  650. string filename = System.IO.Path.GetFileName(cacheEntries[i]);
  651. UInt64 idx = 0;
  652. bool deleteFile = false;
  653. if (UInt64.TryParse(filename, System.Globalization.NumberStyles.AllowHexSpecifier, null, out idx))
  654. {
  655. using (new ReadLock(rwLock))
  656. deleteFile = !UsedIndexes.ContainsKey(idx);
  657. }
  658. else
  659. deleteFile = true;
  660. if (deleteFile)
  661. HTTPManager.IOService.FileDelete(cacheEntries[i]);
  662. }
  663. catch
  664. { }
  665. }
  666. }
  667. #endregion
  668. }
  669. }
  670. #endif