#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