HTTPCacheFileInfo.cs 13 KB

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