#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR) using System; using System.Collections.Generic; #if NETFX_CORE using FileStream = BestHTTP.PlatformSupport.IO.FileStream; using Directory = BestHTTP.PlatformSupport.IO.Directory; using File = BestHTTP.PlatformSupport.IO.File; using BestHTTP.PlatformSupport.IO; #else using FileStream = System.IO.FileStream; using System.IO; #endif namespace BestHTTP.Caching { using BestHTTP.Extensions; /// /// Holds all metadata that need for efficient caching, so we don't need to touch the disk to load headers. /// public class HTTPCacheFileInfo : IComparable { #region Properties /// /// The uri that this HTTPCacheFileInfo belongs to. /// internal Uri Uri { get; set; } /// /// The last access time to this cache entity. The date is in UTC. /// internal DateTime LastAccess { get; set; } /// /// The length of the cache entity's body. /// public int BodyLength { get; set; } /// /// ETag of the entity. /// private string ETag { get; set; } /// /// LastModified date of the entity. /// private string LastModified { get; set; } /// /// When the cache will expire. /// private DateTime Expires { get; set; } /// /// The age that came with the response /// private long Age { get; set; } /// /// Maximum how long the entry should served from the cache without revalidation. /// private long MaxAge { get; set; } /// /// The Date that came with the response. /// private DateTime Date { get; set; } /// /// Indicates whether the entity must be revalidated with the server or can be serverd directly from the cache without touching the server. /// private bool MustRevalidate { get; set; } /// /// The date and time when the HTTPResponse received. /// private DateTime Received { get; set; } /// /// Cached path. /// private string ConstructedPath { get; set; } /// /// This is the index of the entity. Filenames are generated from this value. /// internal UInt64 MappedNameIDX { get; set; } #endregion #region Constructors internal HTTPCacheFileInfo(Uri uri) :this(uri, DateTime.UtcNow, -1) { } internal HTTPCacheFileInfo(Uri uri, DateTime lastAcces, int bodyLength) { this.Uri = uri; this.LastAccess = lastAcces; this.BodyLength = bodyLength; this.MaxAge = -1; this.MappedNameIDX = HTTPCacheService.GetNameIdx(); } internal HTTPCacheFileInfo(Uri uri, System.IO.BinaryReader reader, int version) { this.Uri = uri; this.LastAccess = DateTime.FromBinary(reader.ReadInt64()); this.BodyLength = reader.ReadInt32(); switch(version) { case 2: this.MappedNameIDX = reader.ReadUInt64(); goto case 1; case 1: { this.ETag = reader.ReadString(); this.LastModified = reader.ReadString(); this.Expires = DateTime.FromBinary(reader.ReadInt64()); this.Age = reader.ReadInt64(); this.MaxAge = reader.ReadInt64(); this.Date = DateTime.FromBinary(reader.ReadInt64()); this.MustRevalidate = reader.ReadBoolean(); this.Received = DateTime.FromBinary(reader.ReadInt64()); break; } } } #endregion #region Helper Functions internal void SaveTo(System.IO.BinaryWriter writer) { writer.Write(LastAccess.ToBinary()); writer.Write(BodyLength); writer.Write(MappedNameIDX); writer.Write(ETag); writer.Write(LastModified); writer.Write(Expires.ToBinary()); writer.Write(Age); writer.Write(MaxAge); writer.Write(Date.ToBinary()); writer.Write(MustRevalidate); writer.Write(Received.ToBinary()); } public string GetPath() { if (ConstructedPath != null) return ConstructedPath; return ConstructedPath = System.IO.Path.Combine(HTTPCacheService.CacheFolder, MappedNameIDX.ToString("X")); } public bool IsExists() { if (!HTTPCacheService.IsSupported) return false; return File.Exists(GetPath()); } internal void Delete() { if (!HTTPCacheService.IsSupported) return; string path = GetPath(); try { File.Delete(path); } catch { } finally { Reset(); } } private void Reset() { // MappedNameIDX will remain the same. When we re-save an entity, it will not reset the MappedNameIDX. this.BodyLength = -1; this.ETag = string.Empty; this.Expires = DateTime.FromBinary(0); this.LastModified = string.Empty; this.Age = 0; this.MaxAge = -1; this.Date = DateTime.FromBinary(0); this.MustRevalidate = false; this.Received = DateTime.FromBinary(0); } #endregion #region Caching private void SetUpCachingValues(HTTPResponse response) { response.CacheFileInfo = this; this.ETag = response.GetFirstHeaderValue("ETag").ToStrOrEmpty(); this.Expires = response.GetFirstHeaderValue("Expires").ToDateTime(DateTime.FromBinary(0)); this.LastModified = response.GetFirstHeaderValue("Last-Modified").ToStrOrEmpty(); this.Age = response.GetFirstHeaderValue("Age").ToInt64(0); this.Date = response.GetFirstHeaderValue("Date").ToDateTime(DateTime.FromBinary(0)); string cacheControl = response.GetFirstHeaderValue("cache-control"); if (!string.IsNullOrEmpty(cacheControl)) { string[] kvp = cacheControl.FindOption("max-age"); if (kvp != null) { // Some cache proxies will return float values double maxAge; if (double.TryParse(kvp[1], out maxAge)) this.MaxAge = (int)maxAge; } this.MustRevalidate = cacheControl.ToLower().Contains("must-revalidate"); } this.Received = DateTime.UtcNow; } internal bool WillExpireInTheFuture() { if (!IsExists()) return false; if (MustRevalidate) return false; // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4 : // The max-age directive takes priority over Expires if (MaxAge != -1) { // Age calculation: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.3 long apparent_age = Math.Max(0, (long)(Received - Date).TotalSeconds); long corrected_received_age = Math.Max(apparent_age, Age); long resident_time = (long)(DateTime.UtcNow - Date).TotalSeconds; long current_age = corrected_received_age + resident_time; return current_age < MaxAge; } return Expires > DateTime.UtcNow; } internal void SetUpRevalidationHeaders(HTTPRequest request) { if (!IsExists()) return; // -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). // -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). // -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. if (!string.IsNullOrEmpty(ETag)) request.AddHeader("If-None-Match", ETag); if (!string.IsNullOrEmpty(LastModified)) request.AddHeader("If-Modified-Since", LastModified); } public System.IO.Stream GetBodyStream(out int length) { if (!IsExists()) { length = 0; return null; } length = BodyLength; LastAccess = DateTime.UtcNow; FileStream stream = new FileStream(GetPath(), FileMode.Open, FileAccess.Read, FileShare.Read); stream.Seek(-length, System.IO.SeekOrigin.End); return stream; } internal HTTPResponse ReadResponseTo(HTTPRequest request) { if (!IsExists()) return null; LastAccess = DateTime.UtcNow; using (FileStream stream = new FileStream(GetPath(), FileMode.Open, FileAccess.Read, FileShare.Read)) { var response = new HTTPResponse(request, stream, request.UseStreaming, true); response.CacheFileInfo = this; response.Receive(BodyLength); return response; } } internal void Store(HTTPResponse response) { if (!HTTPCacheService.IsSupported) return; string path = GetPath(); // Path name too long, we don't want to get exceptions if (path.Length > HTTPManager.MaxPathLength) return; if (File.Exists(path)) Delete(); using (FileStream writer = new FileStream(path, FileMode.Create)) { writer.WriteLine("HTTP/1.1 {0} {1}", response.StatusCode, response.Message); foreach (var kvp in response.Headers) { for (int i = 0; i < kvp.Value.Count; ++i) writer.WriteLine("{0}: {1}", kvp.Key, kvp.Value[i]); } writer.WriteLine(); writer.Write(response.Data, 0, response.Data.Length); } BodyLength = response.Data.Length; LastAccess = DateTime.UtcNow; SetUpCachingValues(response); } internal System.IO.Stream GetSaveStream(HTTPResponse response) { if (!HTTPCacheService.IsSupported) return null; LastAccess = DateTime.UtcNow; string path = GetPath(); if (File.Exists(path)) Delete(); // Path name too long, we don't want to get exceptions if (path.Length > HTTPManager.MaxPathLength) return null; // First write out the headers using (FileStream writer = new FileStream(path, FileMode.Create)) { writer.WriteLine("HTTP/1.1 {0} {1}", response.StatusCode, response.Message); foreach (var kvp in response.Headers) { for (int i = 0; i < kvp.Value.Count; ++i) writer.WriteLine("{0}: {1}", kvp.Key, kvp.Value[i]); } writer.WriteLine(); } // If caching is enabled and the response is from cache, and no content-length header set, then we set one to the response. if (response.IsFromCache && !response.Headers.ContainsKey("content-length")) response.Headers.Add("content-length", new List { BodyLength.ToString() }); SetUpCachingValues(response); // then create the stream with Append FileMode return new FileStream(GetPath(), FileMode.Append); } #endregion #region IComparable public int CompareTo(HTTPCacheFileInfo other) { return this.LastAccess.CompareTo(other.LastAccess); } #endregion } } #endif