HTTPCacheFileInfo.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
  2. using System;
  3. using System.Collections.Generic;
  4. #if NETFX_CORE
  5. using FileStream = BestHTTP.PlatformSupport.IO.FileStream;
  6. using Directory = BestHTTP.PlatformSupport.IO.Directory;
  7. using File = BestHTTP.PlatformSupport.IO.File;
  8. using BestHTTP.PlatformSupport.IO;
  9. #else
  10. using FileStream = System.IO.FileStream;
  11. using System.IO;
  12. #endif
  13. namespace BestHTTP.Caching
  14. {
  15. using BestHTTP.Extensions;
  16. /// <summary>
  17. /// Holds all metadata that need for efficient caching, so we don't need to touch the disk to load headers.
  18. /// </summary>
  19. public class HTTPCacheFileInfo : IComparable<HTTPCacheFileInfo>
  20. {
  21. #region Properties
  22. /// <summary>
  23. /// The uri that this HTTPCacheFileInfo belongs to.
  24. /// </summary>
  25. internal Uri Uri { get; set; }
  26. /// <summary>
  27. /// The last access time to this cache entity. The date is in UTC.
  28. /// </summary>
  29. internal DateTime LastAccess { get; set; }
  30. /// <summary>
  31. /// The length of the cache entity's body.
  32. /// </summary>
  33. public int BodyLength { get; set; }
  34. /// <summary>
  35. /// ETag of the entity.
  36. /// </summary>
  37. private string ETag { get; set; }
  38. /// <summary>
  39. /// LastModified date of the entity.
  40. /// </summary>
  41. private string LastModified { get; set; }
  42. /// <summary>
  43. /// When the cache will expire.
  44. /// </summary>
  45. private DateTime Expires { get; set; }
  46. /// <summary>
  47. /// The age that came with the response
  48. /// </summary>
  49. private long Age { get; set; }
  50. /// <summary>
  51. /// Maximum how long the entry should served from the cache without revalidation.
  52. /// </summary>
  53. private long MaxAge { get; set; }
  54. /// <summary>
  55. /// The Date that came with the response.
  56. /// </summary>
  57. private DateTime Date { get; set; }
  58. /// <summary>
  59. /// Indicates whether the entity must be revalidated with the server or can be serverd directly from the cache without touching the server.
  60. /// </summary>
  61. private bool MustRevalidate { get; set; }
  62. /// <summary>
  63. /// The date and time when the HTTPResponse received.
  64. /// </summary>
  65. private DateTime Received { get; set; }
  66. /// <summary>
  67. /// Cached path.
  68. /// </summary>
  69. private string ConstructedPath { get; set; }
  70. /// <summary>
  71. /// This is the index of the entity. Filenames are generated from this value.
  72. /// </summary>
  73. internal UInt64 MappedNameIDX { get; set; }
  74. #endregion
  75. #region Constructors
  76. internal HTTPCacheFileInfo(Uri uri)
  77. :this(uri, DateTime.UtcNow, -1)
  78. {
  79. }
  80. internal HTTPCacheFileInfo(Uri uri, DateTime lastAcces, int bodyLength)
  81. {
  82. this.Uri = uri;
  83. this.LastAccess = lastAcces;
  84. this.BodyLength = bodyLength;
  85. this.MaxAge = -1;
  86. this.MappedNameIDX = HTTPCacheService.GetNameIdx();
  87. }
  88. internal HTTPCacheFileInfo(Uri uri, System.IO.BinaryReader reader, int version)
  89. {
  90. this.Uri = uri;
  91. this.LastAccess = DateTime.FromBinary(reader.ReadInt64());
  92. this.BodyLength = reader.ReadInt32();
  93. switch(version)
  94. {
  95. case 2:
  96. this.MappedNameIDX = reader.ReadUInt64();
  97. goto case 1;
  98. case 1:
  99. {
  100. this.ETag = reader.ReadString();
  101. this.LastModified = reader.ReadString();
  102. this.Expires = DateTime.FromBinary(reader.ReadInt64());
  103. this.Age = reader.ReadInt64();
  104. this.MaxAge = reader.ReadInt64();
  105. this.Date = DateTime.FromBinary(reader.ReadInt64());
  106. this.MustRevalidate = reader.ReadBoolean();
  107. this.Received = DateTime.FromBinary(reader.ReadInt64());
  108. break;
  109. }
  110. }
  111. }
  112. #endregion
  113. #region Helper Functions
  114. internal void SaveTo(System.IO.BinaryWriter writer)
  115. {
  116. writer.Write(LastAccess.ToBinary());
  117. writer.Write(BodyLength);
  118. writer.Write(MappedNameIDX);
  119. writer.Write(ETag);
  120. writer.Write(LastModified);
  121. writer.Write(Expires.ToBinary());
  122. writer.Write(Age);
  123. writer.Write(MaxAge);
  124. writer.Write(Date.ToBinary());
  125. writer.Write(MustRevalidate);
  126. writer.Write(Received.ToBinary());
  127. }
  128. public string GetPath()
  129. {
  130. if (ConstructedPath != null)
  131. return ConstructedPath;
  132. return ConstructedPath = System.IO.Path.Combine(HTTPCacheService.CacheFolder, MappedNameIDX.ToString("X"));
  133. }
  134. public bool IsExists()
  135. {
  136. if (!HTTPCacheService.IsSupported)
  137. return false;
  138. return File.Exists(GetPath());
  139. }
  140. internal void Delete()
  141. {
  142. if (!HTTPCacheService.IsSupported)
  143. return;
  144. string path = GetPath();
  145. try
  146. {
  147. File.Delete(path);
  148. }
  149. catch
  150. { }
  151. finally
  152. {
  153. Reset();
  154. }
  155. }
  156. private void Reset()
  157. {
  158. // MappedNameIDX will remain the same. When we re-save an entity, it will not reset the MappedNameIDX.
  159. this.BodyLength = -1;
  160. this.ETag = string.Empty;
  161. this.Expires = DateTime.FromBinary(0);
  162. this.LastModified = string.Empty;
  163. this.Age = 0;
  164. this.MaxAge = -1;
  165. this.Date = DateTime.FromBinary(0);
  166. this.MustRevalidate = false;
  167. this.Received = DateTime.FromBinary(0);
  168. }
  169. #endregion
  170. #region Caching
  171. private void SetUpCachingValues(HTTPResponse response)
  172. {
  173. response.CacheFileInfo = this;
  174. this.ETag = response.GetFirstHeaderValue("ETag").ToStrOrEmpty();
  175. this.Expires = response.GetFirstHeaderValue("Expires").ToDateTime(DateTime.FromBinary(0));
  176. this.LastModified = response.GetFirstHeaderValue("Last-Modified").ToStrOrEmpty();
  177. this.Age = response.GetFirstHeaderValue("Age").ToInt64(0);
  178. this.Date = response.GetFirstHeaderValue("Date").ToDateTime(DateTime.FromBinary(0));
  179. string cacheControl = response.GetFirstHeaderValue("cache-control");
  180. if (!string.IsNullOrEmpty(cacheControl))
  181. {
  182. string[] kvp = cacheControl.FindOption("max-age");
  183. if (kvp != null)
  184. {
  185. // Some cache proxies will return float values
  186. double maxAge;
  187. if (double.TryParse(kvp[1], out maxAge))
  188. this.MaxAge = (int)maxAge;
  189. }
  190. this.MustRevalidate = cacheControl.ToLower().Contains("must-revalidate");
  191. }
  192. this.Received = DateTime.UtcNow;
  193. }
  194. internal bool WillExpireInTheFuture()
  195. {
  196. if (!IsExists())
  197. return false;
  198. if (MustRevalidate)
  199. return false;
  200. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4 :
  201. // The max-age directive takes priority over Expires
  202. if (MaxAge != -1)
  203. {
  204. // Age calculation:
  205. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.3
  206. long apparent_age = Math.Max(0, (long)(Received - Date).TotalSeconds);
  207. long corrected_received_age = Math.Max(apparent_age, Age);
  208. long resident_time = (long)(DateTime.UtcNow - Date).TotalSeconds;
  209. long current_age = corrected_received_age + resident_time;
  210. return current_age < MaxAge;
  211. }
  212. return Expires > DateTime.UtcNow;
  213. }
  214. internal void SetUpRevalidationHeaders(HTTPRequest request)
  215. {
  216. if (!IsExists())
  217. return;
  218. // -If an entity tag has been provided by the origin server, MUST use that entity tag in any cache-conditional request (using If-Match or If-None-Match).
  219. // -If only a Last-Modified value has been provided by the origin server, SHOULD use that value in non-subrange cache-conditional requests (using If-Modified-Since).
  220. // -If both an entity tag and a Last-Modified value have been provided by the origin server, SHOULD use both validators in cache-conditional requests. This allows both HTTP/1.0 and HTTP/1.1 caches to respond appropriately.
  221. if (!string.IsNullOrEmpty(ETag))
  222. request.AddHeader("If-None-Match", ETag);
  223. if (!string.IsNullOrEmpty(LastModified))
  224. request.AddHeader("If-Modified-Since", LastModified);
  225. }
  226. public System.IO.Stream GetBodyStream(out int length)
  227. {
  228. if (!IsExists())
  229. {
  230. length = 0;
  231. return null;
  232. }
  233. length = BodyLength;
  234. LastAccess = DateTime.UtcNow;
  235. FileStream stream = new FileStream(GetPath(), FileMode.Open, FileAccess.Read, FileShare.Read);
  236. stream.Seek(-length, System.IO.SeekOrigin.End);
  237. return stream;
  238. }
  239. internal HTTPResponse ReadResponseTo(HTTPRequest request)
  240. {
  241. if (!IsExists())
  242. return null;
  243. LastAccess = DateTime.UtcNow;
  244. using (FileStream stream = new FileStream(GetPath(), FileMode.Open, FileAccess.Read, FileShare.Read))
  245. {
  246. var response = new HTTPResponse(request, stream, request.UseStreaming, true);
  247. response.CacheFileInfo = this;
  248. response.Receive(BodyLength);
  249. return response;
  250. }
  251. }
  252. internal void Store(HTTPResponse response)
  253. {
  254. if (!HTTPCacheService.IsSupported)
  255. return;
  256. string path = GetPath();
  257. // Path name too long, we don't want to get exceptions
  258. if (path.Length > HTTPManager.MaxPathLength)
  259. return;
  260. if (File.Exists(path))
  261. Delete();
  262. using (FileStream writer = new FileStream(path, FileMode.Create))
  263. {
  264. writer.WriteLine("HTTP/1.1 {0} {1}", response.StatusCode, response.Message);
  265. foreach (var kvp in response.Headers)
  266. {
  267. for (int i = 0; i < kvp.Value.Count; ++i)
  268. writer.WriteLine("{0}: {1}", kvp.Key, kvp.Value[i]);
  269. }
  270. writer.WriteLine();
  271. writer.Write(response.Data, 0, response.Data.Length);
  272. }
  273. BodyLength = response.Data.Length;
  274. LastAccess = DateTime.UtcNow;
  275. SetUpCachingValues(response);
  276. }
  277. internal System.IO.Stream GetSaveStream(HTTPResponse response)
  278. {
  279. if (!HTTPCacheService.IsSupported)
  280. return null;
  281. LastAccess = DateTime.UtcNow;
  282. string path = GetPath();
  283. if (File.Exists(path))
  284. Delete();
  285. // Path name too long, we don't want to get exceptions
  286. if (path.Length > HTTPManager.MaxPathLength)
  287. return null;
  288. // First write out the headers
  289. using (FileStream writer = new FileStream(path, FileMode.Create))
  290. {
  291. writer.WriteLine("HTTP/1.1 {0} {1}", response.StatusCode, response.Message);
  292. foreach (var kvp in response.Headers)
  293. {
  294. for (int i = 0; i < kvp.Value.Count; ++i)
  295. writer.WriteLine("{0}: {1}", kvp.Key, kvp.Value[i]);
  296. }
  297. writer.WriteLine();
  298. }
  299. // If caching is enabled and the response is from cache, and no content-length header set, then we set one to the response.
  300. if (response.IsFromCache && !response.Headers.ContainsKey("content-length"))
  301. response.Headers.Add("content-length", new List<string> { BodyLength.ToString() });
  302. SetUpCachingValues(response);
  303. // then create the stream with Append FileMode
  304. return new FileStream(GetPath(), FileMode.Append);
  305. }
  306. #endregion
  307. #region IComparable<HTTPCacheFileInfo>
  308. public int CompareTo(HTTPCacheFileInfo other)
  309. {
  310. return this.LastAccess.CompareTo(other.LastAccess);
  311. }
  312. #endregion
  313. }
  314. }
  315. #endif