HTTPCacheService.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Text;
  5. using System.Threading;
  6. #if NETFX_CORE
  7. using FileStream = BestHTTP.PlatformSupport.IO.FileStream;
  8. using Directory = BestHTTP.PlatformSupport.IO.Directory;
  9. using File = BestHTTP.PlatformSupport.IO.File;
  10. using BestHTTP.PlatformSupport.IO;
  11. //Disable CD4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
  12. #pragma warning disable 4014
  13. #else
  14. using FileStream = System.IO.FileStream;
  15. using Directory = System.IO.Directory;
  16. using System.IO;
  17. #endif
  18. //
  19. // Version 1: Initial release
  20. // Version 2: Filenames are generated from an index.
  21. //
  22. namespace BestHTTP.Caching
  23. {
  24. using BestHTTP.Extensions;
  25. public sealed class UriComparer : IEqualityComparer<Uri>
  26. {
  27. public bool Equals(Uri x, Uri y)
  28. {
  29. return Uri.Compare(x, y, UriComponents.HttpRequestUrl, UriFormat.SafeUnescaped, StringComparison.Ordinal) == 0;
  30. }
  31. public int GetHashCode(Uri uri)
  32. {
  33. return uri.ToString().GetHashCode();
  34. }
  35. }
  36. public static class HTTPCacheService
  37. {
  38. #region Properties & Fields
  39. /// <summary>
  40. /// Library file-format versioning support
  41. /// </summary>
  42. private const int LibraryVersion = 2;
  43. public static bool IsSupported
  44. {
  45. get
  46. {
  47. if (IsSupportCheckDone)
  48. return isSupported;
  49. try
  50. {
  51. File.Exists(HTTPManager.GetRootCacheFolder());
  52. isSupported = true;
  53. }
  54. catch
  55. {
  56. isSupported = false;
  57. HTTPManager.Logger.Warning("HTTPCacheService", "Cache Service Disabled!");
  58. }
  59. finally
  60. {
  61. IsSupportCheckDone = true;
  62. }
  63. return isSupported;
  64. }
  65. }
  66. private static bool isSupported;
  67. private static bool IsSupportCheckDone;
  68. private static Dictionary<Uri, HTTPCacheFileInfo> library;
  69. private static Dictionary<Uri, HTTPCacheFileInfo> Library { get { LoadLibrary(); return library; } }
  70. private static Dictionary<UInt64, HTTPCacheFileInfo> UsedIndexes = new Dictionary<ulong, HTTPCacheFileInfo>();
  71. internal static string CacheFolder { get; private set; }
  72. private static string LibraryPath { get; set; }
  73. private static bool InClearThread;
  74. private static bool 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 (!Directory.Exists(CacheFolder))
  107. Directory.CreateDirectory(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. lock(Library)
  120. {
  121. UInt64 result = NextNameIDX;
  122. do
  123. {
  124. NextNameIDX = ++NextNameIDX % UInt64.MaxValue;
  125. } while (UsedIndexes.ContainsKey(NextNameIDX));
  126. return result;
  127. }
  128. }
  129. internal static bool HasEntity(Uri uri)
  130. {
  131. if (!IsSupported)
  132. return false;
  133. lock (Library)
  134. return Library.ContainsKey(uri);
  135. }
  136. internal static bool DeleteEntity(Uri uri, bool removeFromLibrary = true)
  137. {
  138. if (!IsSupported)
  139. return false;
  140. object uriLocker = HTTPCacheFileLock.Acquire(uri);
  141. // Just use lock now: http://forum.unity3d.com/threads/4-6-ios-64-bit-beta.290551/page-6#post-1937033
  142. // To avoid a dead-lock we try acquire the lock on this uri only for a little time.
  143. // If we can't acquire it, its better to just return without risking a deadlock.
  144. //if (Monitor.TryEnter(uriLocker, TimeSpan.FromSeconds(0.5f)))
  145. lock(uriLocker)
  146. {
  147. try
  148. {
  149. lock (Library)
  150. {
  151. HTTPCacheFileInfo info;
  152. bool inStats = Library.TryGetValue(uri, out info);
  153. if (inStats)
  154. info.Delete();
  155. if (inStats && removeFromLibrary)
  156. {
  157. Library.Remove(uri);
  158. UsedIndexes.Remove(info.MappedNameIDX);
  159. }
  160. return true;
  161. }
  162. }
  163. finally
  164. {
  165. //Monitor.Exit(uriLocker);
  166. }
  167. }
  168. //return false;
  169. }
  170. internal static bool IsCachedEntityExpiresInTheFuture(HTTPRequest request)
  171. {
  172. if (!IsSupported)
  173. return false;
  174. HTTPCacheFileInfo info;
  175. lock (Library)
  176. if (Library.TryGetValue(request.CurrentUri, out info))
  177. return info.WillExpireInTheFuture();
  178. return false;
  179. }
  180. /// <summary>
  181. /// Utility function to set the cache control headers according to the spec.: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
  182. /// </summary>
  183. /// <param name="request"></param>
  184. internal static void SetHeaders(HTTPRequest request)
  185. {
  186. if (!IsSupported)
  187. return;
  188. HTTPCacheFileInfo info;
  189. lock (Library)
  190. if (Library.TryGetValue(request.CurrentUri, out info))
  191. info.SetUpRevalidationHeaders(request);
  192. }
  193. #endregion
  194. #region Get Functions
  195. internal static HTTPCacheFileInfo GetEntity(Uri uri)
  196. {
  197. if (!IsSupported)
  198. return null;
  199. HTTPCacheFileInfo info = null;
  200. lock (Library)
  201. Library.TryGetValue(uri, out info);
  202. return info;
  203. }
  204. internal static HTTPResponse GetFullResponse(HTTPRequest request)
  205. {
  206. if (!IsSupported)
  207. return null;
  208. HTTPCacheFileInfo info;
  209. lock (Library)
  210. if (Library.TryGetValue(request.CurrentUri, out info))
  211. return info.ReadResponseTo(request);
  212. return null;
  213. }
  214. #endregion
  215. #region Storing
  216. /// <summary>
  217. /// Checks if the given response can be cached. http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
  218. /// </summary>
  219. /// <returns>Returns true if cacheable, false otherwise.</returns>
  220. internal static bool IsCacheble(Uri uri, HTTPMethods method, HTTPResponse response)
  221. {
  222. if (!IsSupported)
  223. return false;
  224. if (method != HTTPMethods.Get)
  225. return false;
  226. if (response == null)
  227. return false;
  228. // https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.12 - Cache Replacement
  229. // 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.
  230. //if (response.StatusCode == 304)
  231. // return false;
  232. if (response.StatusCode < 200 || response.StatusCode >= 400)
  233. return false;
  234. //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
  235. var cacheControls = response.GetHeaderValues("cache-control");
  236. if (cacheControls != null)
  237. {
  238. if (cacheControls.Exists(headerValue => {
  239. string value = headerValue.ToLower();
  240. return value.Contains("no-store") || value.Contains("no-cache");
  241. }))
  242. return false;
  243. }
  244. var pragmas = response.GetHeaderValues("pragma");
  245. if (pragmas != null)
  246. {
  247. if (pragmas.Exists(headerValue => {
  248. string value = headerValue.ToLower();
  249. return value.Contains("no-store") || value.Contains("no-cache");
  250. }))
  251. return false;
  252. }
  253. // Responses with byte ranges not supported yet.
  254. var byteRanges = response.GetHeaderValues("content-range");
  255. if (byteRanges != null)
  256. return false;
  257. return true;
  258. }
  259. internal static HTTPCacheFileInfo Store(Uri uri, HTTPMethods method, HTTPResponse response)
  260. {
  261. if (response == null || response.Data == null || response.Data.Length == 0)
  262. return null;
  263. if (!IsSupported)
  264. return null;
  265. HTTPCacheFileInfo info = null;
  266. lock (Library)
  267. {
  268. if (!Library.TryGetValue(uri, out info))
  269. {
  270. Library.Add(uri, info = new HTTPCacheFileInfo(uri));
  271. UsedIndexes.Add(info.MappedNameIDX, info);
  272. }
  273. try
  274. {
  275. info.Store(response);
  276. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  277. HTTPManager.Logger.Verbose("HTTPCacheService", string.Format("{0} - Saved to cache", uri.ToString()));
  278. }
  279. catch
  280. {
  281. // If something happens while we write out the response, than we will delete it because it might be in an invalid state.
  282. DeleteEntity(uri);
  283. throw;
  284. }
  285. }
  286. return info;
  287. }
  288. internal static System.IO.Stream PrepareStreamed(Uri uri, HTTPResponse response)
  289. {
  290. if (!IsSupported)
  291. return null;
  292. HTTPCacheFileInfo info;
  293. lock (Library)
  294. {
  295. if (!Library.TryGetValue(uri, out info))
  296. {
  297. Library.Add(uri, info = new HTTPCacheFileInfo(uri));
  298. UsedIndexes.Add(info.MappedNameIDX, info);
  299. }
  300. try
  301. {
  302. return info.GetSaveStream(response);
  303. }
  304. catch
  305. {
  306. // If something happens while we write out the response, than we will delete it because it might be in an invalid state.
  307. DeleteEntity(uri);
  308. throw;
  309. }
  310. }
  311. }
  312. #endregion
  313. #region Public Maintenance Functions
  314. /// <summary>
  315. /// Deletes all cache entity. Non blocking.
  316. /// <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>
  317. /// </summary>
  318. public static void BeginClear()
  319. {
  320. if (!IsSupported)
  321. return;
  322. if (InClearThread)
  323. return;
  324. InClearThread = true;
  325. SetupCacheFolder();
  326. #if !NETFX_CORE
  327. ThreadPool.QueueUserWorkItem(new WaitCallback((param) => ClearImpl(param)));
  328. //new Thread(ClearImpl).Start();
  329. #else
  330. #pragma warning disable 4014
  331. Windows.System.Threading.ThreadPool.RunAsync(ClearImpl);
  332. #pragma warning restore 4014
  333. #endif
  334. }
  335. private static void ClearImpl(object param)
  336. {
  337. if (!IsSupported)
  338. return;
  339. try
  340. {
  341. // GetFiles will return a string array that contains the files in the folder with the full path
  342. string[] cacheEntries = Directory.GetFiles(CacheFolder);
  343. for (int i = 0; i < cacheEntries.Length; ++i)
  344. {
  345. // 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.
  346. // So while there might be some problem with any file, we don't want to abort the whole for loop
  347. try
  348. {
  349. File.Delete(cacheEntries[i]);
  350. }
  351. catch
  352. { }
  353. }
  354. }
  355. finally
  356. {
  357. UsedIndexes.Clear();
  358. library.Clear();
  359. NextNameIDX = 0x0001;
  360. SaveLibrary();
  361. InClearThread = false;
  362. }
  363. }
  364. /// <summary>
  365. /// Deletes all expired cache entity.
  366. /// <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>
  367. /// </summary>
  368. public static void BeginMaintainence(HTTPCacheMaintananceParams maintananceParam)
  369. {
  370. if (maintananceParam == null)
  371. throw new ArgumentNullException("maintananceParams == null");
  372. if (!HTTPCacheService.IsSupported)
  373. return;
  374. if (InMaintainenceThread)
  375. return;
  376. InMaintainenceThread = true;
  377. SetupCacheFolder();
  378. #if !NETFX_CORE
  379. ThreadPool.QueueUserWorkItem(new WaitCallback((param) =>
  380. //new Thread((param) =>
  381. #else
  382. #pragma warning disable 4014
  383. Windows.System.Threading.ThreadPool.RunAsync((param) =>
  384. #pragma warning restore 4014
  385. #endif
  386. {
  387. try
  388. {
  389. lock (Library)
  390. {
  391. // Delete cache entries older than the given time.
  392. DateTime deleteOlderAccessed = DateTime.UtcNow - maintananceParam.DeleteOlder;
  393. List<HTTPCacheFileInfo> removedEntities = new List<HTTPCacheFileInfo>();
  394. foreach (var kvp in Library)
  395. if (kvp.Value.LastAccess < deleteOlderAccessed)
  396. {
  397. if (DeleteEntity(kvp.Key, false))
  398. removedEntities.Add(kvp.Value);
  399. }
  400. for (int i = 0; i < removedEntities.Count; ++i)
  401. {
  402. Library.Remove(removedEntities[i].Uri);
  403. UsedIndexes.Remove(removedEntities[i].MappedNameIDX);
  404. }
  405. removedEntities.Clear();
  406. ulong cacheSize = GetCacheSize();
  407. // This step will delete all entries starting with the oldest LastAccess property while the cache size greater then the MaxCacheSize in the given param.
  408. if (cacheSize > maintananceParam.MaxCacheSize)
  409. {
  410. List<HTTPCacheFileInfo> fileInfos = new List<HTTPCacheFileInfo>(library.Count);
  411. foreach(var kvp in library)
  412. fileInfos.Add(kvp.Value);
  413. fileInfos.Sort();
  414. int idx = 0;
  415. while (cacheSize >= maintananceParam.MaxCacheSize && idx < fileInfos.Count)
  416. {
  417. try
  418. {
  419. var fi = fileInfos[idx];
  420. ulong length = (ulong)fi.BodyLength;
  421. DeleteEntity(fi.Uri);
  422. cacheSize -= length;
  423. }
  424. catch
  425. {}
  426. finally
  427. {
  428. ++idx;
  429. }
  430. }
  431. }
  432. }
  433. }
  434. finally
  435. {
  436. SaveLibrary();
  437. InMaintainenceThread = false;
  438. }
  439. }
  440. #if !NETFX_CORE
  441. ));
  442. #else
  443. );
  444. #endif
  445. }
  446. public static int GetCacheEntityCount()
  447. {
  448. if (!HTTPCacheService.IsSupported)
  449. return 0;
  450. CheckSetup();
  451. lock(Library)
  452. return Library.Count;
  453. }
  454. public static ulong GetCacheSize()
  455. {
  456. ulong size = 0;
  457. if (!IsSupported)
  458. return size;
  459. CheckSetup();
  460. lock (Library)
  461. foreach (var kvp in Library)
  462. if (kvp.Value.BodyLength > 0)
  463. size += (ulong)kvp.Value.BodyLength;
  464. return size;
  465. }
  466. #endregion
  467. #region Cache Library Management
  468. private static void LoadLibrary()
  469. {
  470. // Already loaded?
  471. if (library != null)
  472. return;
  473. if (!IsSupported)
  474. return;
  475. library = new Dictionary<Uri, HTTPCacheFileInfo>(new UriComparer());
  476. if (!File.Exists(LibraryPath))
  477. {
  478. DeleteUnusedFiles();
  479. return;
  480. }
  481. try
  482. {
  483. int version;
  484. lock (library)
  485. {
  486. using (var fs = new FileStream(LibraryPath, FileMode.Open))
  487. using (var br = new System.IO.BinaryReader(fs))
  488. {
  489. version = br.ReadInt32();
  490. if (version > 1)
  491. NextNameIDX = br.ReadUInt64();
  492. int statCount = br.ReadInt32();
  493. for (int i = 0; i < statCount; ++i)
  494. {
  495. Uri uri = new Uri(br.ReadString());
  496. var entity = new HTTPCacheFileInfo(uri, br, version);
  497. if (entity.IsExists())
  498. {
  499. library.Add(uri, entity);
  500. if (version > 1)
  501. UsedIndexes.Add(entity.MappedNameIDX, entity);
  502. }
  503. }
  504. }
  505. }
  506. if (version == 1)
  507. BeginClear();
  508. else
  509. DeleteUnusedFiles();
  510. }
  511. catch
  512. {}
  513. }
  514. internal static void SaveLibrary()
  515. {
  516. if (library == null)
  517. return;
  518. if (!IsSupported)
  519. return;
  520. try
  521. {
  522. lock (Library)
  523. {
  524. using (var fs = new FileStream(LibraryPath, FileMode.Create))
  525. using (var bw = new System.IO.BinaryWriter(fs))
  526. {
  527. bw.Write(LibraryVersion);
  528. bw.Write(NextNameIDX);
  529. bw.Write(Library.Count);
  530. foreach (var kvp in Library)
  531. {
  532. bw.Write(kvp.Key.ToString());
  533. kvp.Value.SaveTo(bw);
  534. }
  535. }
  536. }
  537. }
  538. catch
  539. {}
  540. }
  541. internal static void SetBodyLength(Uri uri, int bodyLength)
  542. {
  543. if (!IsSupported)
  544. return;
  545. lock (Library)
  546. {
  547. HTTPCacheFileInfo fileInfo;
  548. if (Library.TryGetValue(uri, out fileInfo))
  549. fileInfo.BodyLength = bodyLength;
  550. else
  551. {
  552. Library.Add(uri, fileInfo = new HTTPCacheFileInfo(uri, DateTime.UtcNow, bodyLength));
  553. UsedIndexes.Add(fileInfo.MappedNameIDX, fileInfo);
  554. }
  555. }
  556. }
  557. /// <summary>
  558. /// Deletes all files from the cache folder that isn't in the Library.
  559. /// </summary>
  560. private static void DeleteUnusedFiles()
  561. {
  562. if (!IsSupported)
  563. return;
  564. CheckSetup();
  565. // GetFiles will return a string array that contains the files in the folder with the full path
  566. string[] cacheEntries = Directory.GetFiles(CacheFolder);
  567. for (int i = 0; i < cacheEntries.Length; ++i)
  568. {
  569. // 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.
  570. // So while there might be some problem with any file, we don't want to abort the whole for loop
  571. try
  572. {
  573. string filename = System.IO.Path.GetFileName(cacheEntries[i]);
  574. UInt64 idx = 0;
  575. bool deleteFile = false;
  576. if (UInt64.TryParse(filename, System.Globalization.NumberStyles.AllowHexSpecifier, null, out idx))
  577. lock (Library)
  578. deleteFile = !UsedIndexes.ContainsKey(idx);
  579. else
  580. deleteFile = true;
  581. if (deleteFile)
  582. File.Delete(cacheEntries[i]);
  583. }
  584. catch
  585. {}
  586. }
  587. }
  588. #endregion
  589. }
  590. }
  591. #endif