mirror of
https://gitee.com/jisol/jisol-game/
synced 2025-09-27 02:36:14 +00:00
提交Unity 联机Pro
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
#if !BESTHTTP_DISABLE_CACHING
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace BestHTTP.Caching
|
||||
{
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.PlatformSupport.FileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Holds all metadata that need for efficient caching, so we don't need to touch the disk to load headers.
|
||||
/// </summary>
|
||||
public class HTTPCacheFileInfo : IComparable<HTTPCacheFileInfo>
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The uri that this HTTPCacheFileInfo belongs to.
|
||||
/// </summary>
|
||||
public Uri Uri { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last access time to this cache entity. The date is in UTC.
|
||||
/// </summary>
|
||||
public DateTime LastAccess { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the cache entity's body.
|
||||
/// </summary>
|
||||
public int BodyLength { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// ETag of the entity.
|
||||
/// </summary>
|
||||
public string ETag { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// LastModified date of the entity.
|
||||
/// </summary>
|
||||
public string LastModified { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the cache will expire.
|
||||
/// </summary>
|
||||
public DateTime Expires { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The age that came with the response
|
||||
/// </summary>
|
||||
public long Age { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum how long the entry should served from the cache without revalidation.
|
||||
/// </summary>
|
||||
public long MaxAge { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Date that came with the response.
|
||||
/// </summary>
|
||||
public DateTime Date { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the entity must be revalidated with the server or can be serverd directly from the cache without touching the server when the content is considered stale.
|
||||
/// </summary>
|
||||
public bool MustRevalidate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If it's true, the client always have to revalidate the cached content when it's stale.
|
||||
/// </summary>
|
||||
public bool NoCache { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// It's a grace period to serve staled content without revalidation.
|
||||
/// </summary>
|
||||
public long StaleWhileRevalidate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows the client to serve stale content if the server responds with an 5xx error.
|
||||
/// </summary>
|
||||
public long StaleIfError { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date and time when the HTTPResponse received.
|
||||
/// </summary>
|
||||
public DateTime Received { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached path.
|
||||
/// </summary>
|
||||
public string ConstructedPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the index of the entity. Filenames are generated from this value.
|
||||
/// </summary>
|
||||
internal UInt64 MappedNameIDX { get; private 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 3:
|
||||
this.NoCache = reader.ReadBoolean();
|
||||
this.StaleWhileRevalidate = reader.ReadInt64();
|
||||
this.StaleIfError = reader.ReadInt64();
|
||||
goto case 2;
|
||||
|
||||
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)
|
||||
{
|
||||
// base
|
||||
writer.Write(this.LastAccess.ToBinary());
|
||||
writer.Write(this.BodyLength);
|
||||
|
||||
// version 3
|
||||
writer.Write(this.NoCache);
|
||||
writer.Write(this.StaleWhileRevalidate);
|
||||
writer.Write(this.StaleIfError);
|
||||
|
||||
// version 2
|
||||
writer.Write(this.MappedNameIDX);
|
||||
|
||||
// version 1
|
||||
writer.Write(this.ETag);
|
||||
writer.Write(this.LastModified);
|
||||
writer.Write(this.Expires.ToBinary());
|
||||
writer.Write(this.Age);
|
||||
writer.Write(this.MaxAge);
|
||||
writer.Write(this.Date.ToBinary());
|
||||
writer.Write(this.MustRevalidate);
|
||||
writer.Write(this.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 HTTPManager.IOService.FileExists(GetPath());
|
||||
}
|
||||
|
||||
internal void Delete()
|
||||
{
|
||||
if (!HTTPCacheService.IsSupported)
|
||||
return;
|
||||
|
||||
string path = GetPath();
|
||||
try
|
||||
{
|
||||
HTTPManager.IOService.FileDelete(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);
|
||||
this.NoCache = false;
|
||||
this.StaleWhileRevalidate = 0;
|
||||
this.StaleIfError = 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Caching
|
||||
|
||||
internal void SetUpCachingValues(HTTPResponse response)
|
||||
{
|
||||
response.CacheFileInfo = this;
|
||||
|
||||
this.ETag = response.GetFirstHeaderValue("ETag").ToStr(this.ETag ?? string.Empty);
|
||||
this.Expires = response.GetFirstHeaderValue("Expires").ToDateTime(this.Expires);
|
||||
this.LastModified = response.GetFirstHeaderValue("Last-Modified").ToStr(this.LastModified ?? string.Empty);
|
||||
|
||||
this.Age = response.GetFirstHeaderValue("Age").ToInt64(this.Age);
|
||||
|
||||
this.Date = response.GetFirstHeaderValue("Date").ToDateTime(this.Date);
|
||||
|
||||
List<string> cacheControls = response.GetHeaderValues("cache-control");
|
||||
if (cacheControls != null && cacheControls.Count > 0)
|
||||
{
|
||||
// Merge all Cache-Control header values into one
|
||||
string cacheControl = cacheControls[0];
|
||||
for (int i = 1; i < cacheControls.Count; ++i)
|
||||
cacheControl += "," + cacheControls[i];
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheControl))
|
||||
{
|
||||
HeaderParser parser = new HeaderParser(cacheControl);
|
||||
|
||||
if (parser.Values != null)
|
||||
{
|
||||
for (int i = 0; i < parser.Values.Count; ++i)
|
||||
{
|
||||
var kvp = parser.Values[i];
|
||||
|
||||
switch(kvp.Key.ToLowerInvariant())
|
||||
{
|
||||
case "max-age":
|
||||
if (kvp.HasValue)
|
||||
{
|
||||
// Some cache proxies will return float values
|
||||
double maxAge;
|
||||
if (double.TryParse(kvp.Value, out maxAge))
|
||||
this.MaxAge = (int)maxAge;
|
||||
else
|
||||
this.MaxAge = 0;
|
||||
}
|
||||
else
|
||||
this.MaxAge = 0;
|
||||
break;
|
||||
|
||||
case "stale-while-revalidate":
|
||||
this.StaleWhileRevalidate = kvp.HasValue ? kvp.Value.ToInt64(0) : 0;
|
||||
break;
|
||||
|
||||
case "stale-if-error":
|
||||
this.StaleIfError = kvp.HasValue ? kvp.Value.ToInt64(0) : 0;
|
||||
break;
|
||||
|
||||
case "must-revalidate":
|
||||
this.MustRevalidate = true;
|
||||
break;
|
||||
|
||||
case "no-cache":
|
||||
this.NoCache = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//string[] options = cacheControl.ToLowerInvariant().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
//
|
||||
//string[] kvp = options.FindOption("max-age");
|
||||
//if (kvp != null && kvp.Length > 1)
|
||||
//{
|
||||
// // Some cache proxies will return float values
|
||||
// double maxAge;
|
||||
// if (double.TryParse(kvp[1], out maxAge))
|
||||
// this.MaxAge = (int)maxAge;
|
||||
// else
|
||||
// this.MaxAge = 0;
|
||||
//}
|
||||
//else
|
||||
// this.MaxAge = 0;
|
||||
//
|
||||
//kvp = options.FindOption("stale-while-revalidate");
|
||||
//if (kvp != null && kvp.Length == 2 && !string.IsNullOrEmpty(kvp[1]))
|
||||
// this.StaleWhileRevalidate = kvp[1].ToInt64(0);
|
||||
//
|
||||
//kvp = options.FindOption("stale-if-error");
|
||||
//if (kvp != null && kvp.Length == 2 && !string.IsNullOrEmpty(kvp[1]))
|
||||
// this.StaleIfError = kvp[1].ToInt64(0);
|
||||
//
|
||||
//this.MustRevalidate = cacheControl.Contains("must-revalidate");
|
||||
//this.NoCache = cacheControl.Contains("no-cache");
|
||||
}
|
||||
}
|
||||
|
||||
this.Received = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// isInError should be true if downloading the content fails, and in that case, it might extend the content's freshness
|
||||
/// </summary>
|
||||
public bool WillExpireInTheFuture(bool isInError)
|
||||
{
|
||||
if (!IsExists())
|
||||
return false;
|
||||
|
||||
// https://csswizardry.com/2019/03/cache-control-for-civilians/#no-cache
|
||||
// no-cache will always hit the network as it has to revalidate with the server before it can release the browser’s cached copy (unless the server responds with a fresher response),
|
||||
// but if the server responds favourably, the network transfer is only a file’s headers: the body can be grabbed from cache rather than redownloaded.
|
||||
if (this.NoCache)
|
||||
return false;
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4 :
|
||||
// The max-age directive takes priority over Expires
|
||||
if (MaxAge > 0)
|
||||
{
|
||||
// Age calculation:
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.3
|
||||
|
||||
long apparent_age = Math.Max(0, (long)(this.Received - this.Date).TotalSeconds);
|
||||
long corrected_received_age = Math.Max(apparent_age, this.Age);
|
||||
long resident_time = (long)(DateTime.UtcNow - this.Date).TotalSeconds;
|
||||
long current_age = corrected_received_age + resident_time;
|
||||
|
||||
long maxAge = this.MaxAge + (this.NoCache ? 0 : this.StaleWhileRevalidate) + (isInError ? this.StaleIfError : 0);
|
||||
|
||||
return current_age < maxAge || this.Expires > DateTime.UtcNow;
|
||||
}
|
||||
|
||||
return this.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.SetHeader("If-None-Match", ETag);
|
||||
|
||||
if (!string.IsNullOrEmpty(LastModified))
|
||||
request.SetHeader("If-Modified-Since", LastModified);
|
||||
}
|
||||
|
||||
public System.IO.Stream GetBodyStream(out int length)
|
||||
{
|
||||
if (!IsExists())
|
||||
{
|
||||
length = 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
length = BodyLength;
|
||||
|
||||
LastAccess = DateTime.UtcNow;
|
||||
|
||||
Stream stream = HTTPManager.IOService.CreateFileStream(GetPath(), FileStreamModes.OpenRead);
|
||||
stream.Seek(-length, System.IO.SeekOrigin.End);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
internal void ReadResponseTo(HTTPRequest request)
|
||||
{
|
||||
if (!IsExists())
|
||||
return;
|
||||
|
||||
LastAccess = DateTime.UtcNow;
|
||||
|
||||
using (Stream stream = HTTPManager.IOService.CreateFileStream(GetPath(), FileStreamModes.OpenRead))
|
||||
{
|
||||
request.Response = new HTTPResponse(request, stream, request.UseStreaming, true);
|
||||
request.Response.CacheFileInfo = this;
|
||||
request.Response.Receive(BodyLength);
|
||||
}
|
||||
}
|
||||
|
||||
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 (HTTPManager.IOService.FileExists(path))
|
||||
Delete();
|
||||
|
||||
using (Stream writer = HTTPManager.IOService.CreateFileStream(GetPath(), FileStreamModes.Create))
|
||||
{
|
||||
writer.WriteLine("HTTP/{0}.{1} {2} {3}", response.VersionMajor, response.VersionMinor, 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 (HTTPManager.IOService.FileExists(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 (Stream writer = HTTPManager.IOService.CreateFileStream(GetPath(), FileStreamModes.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.HasHeader("content-length"))
|
||||
response.AddHeader("content-length", BodyLength.ToString());
|
||||
|
||||
SetUpCachingValues(response);
|
||||
|
||||
// then create the stream with Append FileMode
|
||||
return HTTPManager.IOService.CreateFileStream(GetPath(), FileStreamModes.Append);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IComparable<HTTPCacheFileInfo>
|
||||
|
||||
public int CompareTo(HTTPCacheFileInfo other)
|
||||
{
|
||||
return this.LastAccess.CompareTo(other.LastAccess);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f50da5c22225384fbe271e0453fe545
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,43 @@
|
||||
#if !BESTHTTP_DISABLE_CACHING
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BestHTTP.Caching
|
||||
{
|
||||
//static class HTTPCacheFileLock
|
||||
//{
|
||||
// private static Dictionary<Uri, object> FileLocks = new Dictionary<Uri, object>();
|
||||
// //private static object SyncRoot = new object();
|
||||
// private static System.Threading.ReaderWriterLockSlim rwLock = new System.Threading.ReaderWriterLockSlim(System.Threading.LockRecursionPolicy.NoRecursion);
|
||||
//
|
||||
// internal static object Acquire(Uri uri)
|
||||
// {
|
||||
// rwLock.EnterUpgradeableReadLock();
|
||||
// try
|
||||
// {
|
||||
// object fileLock;
|
||||
// if (!FileLocks.TryGetValue(uri, out fileLock))
|
||||
// {
|
||||
// rwLock.EnterWriteLock();
|
||||
// try
|
||||
// {
|
||||
// FileLocks.Add(uri, fileLock = new object());
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// rwLock.ExitWriteLock();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return fileLock;
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// rwLock.ExitUpgradeableReadLock();
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b141dab023cd9d438e0585b93cf2826
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,27 @@
|
||||
#if !BESTHTTP_DISABLE_CACHING
|
||||
|
||||
using System;
|
||||
|
||||
namespace BestHTTP.Caching
|
||||
{
|
||||
public sealed class HTTPCacheMaintananceParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Delete cache entries that accessed older then this value. If TimeSpan.FromSeconds(0) is used then all cache entries will be deleted. With TimeSpan.FromDays(2) entries that older then two days will be deleted.
|
||||
/// </summary>
|
||||
public TimeSpan DeleteOlder { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the cache is larger then the MaxCacheSize after the first maintanance step, then the maintanance job will forcedelete cache entries starting with the oldest last accessed one.
|
||||
/// </summary>
|
||||
public ulong MaxCacheSize { get; private set; }
|
||||
|
||||
public HTTPCacheMaintananceParams(TimeSpan deleteOlder, ulong maxCacheSize)
|
||||
{
|
||||
this.DeleteOlder = deleteOlder;
|
||||
this.MaxCacheSize = maxCacheSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ce5552e539e38647a8f37ab36951bf7
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,838 @@
|
||||
#if !BESTHTTP_DISABLE_CACHING
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
//
|
||||
// Version 1: Initial release
|
||||
// Version 2: Filenames are generated from an index.
|
||||
//
|
||||
|
||||
namespace BestHTTP.Caching
|
||||
{
|
||||
using BestHTTP.Core;
|
||||
using BestHTTP.Extensions;
|
||||
using BestHTTP.PlatformSupport.FileSystem;
|
||||
using BestHTTP.PlatformSupport.Threading;
|
||||
|
||||
public sealed class UriComparer : IEqualityComparer<Uri>
|
||||
{
|
||||
public bool Equals(Uri x, Uri y)
|
||||
{
|
||||
return Uri.Compare(x, y, UriComponents.HttpRequestUrl, UriFormat.SafeUnescaped, StringComparison.Ordinal) == 0;
|
||||
}
|
||||
|
||||
public int GetHashCode(Uri uri)
|
||||
{
|
||||
return uri.ToString().GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class HTTPCacheService
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
/// <summary>
|
||||
/// Library file-format versioning support
|
||||
/// </summary>
|
||||
private const int LibraryVersion = 3;
|
||||
|
||||
public static bool IsSupported
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsSupportCheckDone)
|
||||
return isSupported;
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
// Explicitly disable cahing under WebGL
|
||||
isSupported = false;
|
||||
#else
|
||||
// If DirectoryExists throws an exception we will set IsSupprted to false
|
||||
HTTPManager.IOService.DirectoryExists(HTTPManager.GetRootCacheFolder());
|
||||
isSupported = true;
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
isSupported = false;
|
||||
|
||||
HTTPManager.Logger.Warning("HTTPCacheService", "Cache Service Disabled!");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSupportCheckDone = true;
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
}
|
||||
private static bool isSupported;
|
||||
private static bool IsSupportCheckDone;
|
||||
|
||||
private static Dictionary<Uri, HTTPCacheFileInfo> library;
|
||||
|
||||
private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
|
||||
|
||||
private static Dictionary<UInt64, HTTPCacheFileInfo> UsedIndexes = new Dictionary<ulong, HTTPCacheFileInfo>();
|
||||
|
||||
internal static string CacheFolder { get; private set; }
|
||||
private static string LibraryPath { get; set; }
|
||||
|
||||
private volatile static bool InClearThread;
|
||||
private volatile static bool InMaintainenceThread;
|
||||
|
||||
/// <summary>
|
||||
/// This property returns true while the service is in a Clear or Maintenance thread.
|
||||
/// </summary>
|
||||
public static bool IsDoingMaintainence { get { return InClearThread || InMaintainenceThread; } }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the index of the next stored entity. The entity's file name is generated from this index.
|
||||
/// </summary>
|
||||
private static UInt64 NextNameIDX;
|
||||
|
||||
#endregion
|
||||
|
||||
static HTTPCacheService()
|
||||
{
|
||||
NextNameIDX = 0x0001;
|
||||
}
|
||||
|
||||
#region Common Functions
|
||||
|
||||
internal static void CheckSetup()
|
||||
{
|
||||
if (!HTTPCacheService.IsSupported)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
SetupCacheFolder();
|
||||
LoadLibrary();
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
|
||||
internal static void SetupCacheFolder()
|
||||
{
|
||||
if (!HTTPCacheService.IsSupported)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(CacheFolder) || string.IsNullOrEmpty(LibraryPath))
|
||||
{
|
||||
CacheFolder = System.IO.Path.Combine(HTTPManager.GetRootCacheFolder(), "HTTPCache");
|
||||
if (!HTTPManager.IOService.DirectoryExists(CacheFolder))
|
||||
HTTPManager.IOService.DirectoryCreate(CacheFolder);
|
||||
|
||||
LibraryPath = System.IO.Path.Combine(HTTPManager.GetRootCacheFolder(), "Library");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
isSupported = false;
|
||||
|
||||
HTTPManager.Logger.Warning("HTTPCacheService", "Cache Service Disabled!");
|
||||
}
|
||||
}
|
||||
|
||||
internal static UInt64 GetNameIdx()
|
||||
{
|
||||
UInt64 result = NextNameIDX;
|
||||
|
||||
do
|
||||
{
|
||||
NextNameIDX = ++NextNameIDX % UInt64.MaxValue;
|
||||
} while (UsedIndexes.ContainsKey(NextNameIDX));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool HasEntity(Uri uri)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return false;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
using (new ReadLock(rwLock))
|
||||
return library.ContainsKey(uri);
|
||||
}
|
||||
|
||||
public static bool DeleteEntity(Uri uri, bool removeFromLibrary = true)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return false;
|
||||
|
||||
// 2019.05.10: Removed all locking except the one on the library.
|
||||
|
||||
CheckSetup();
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
DeleteEntityImpl(uri, removeFromLibrary, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteEntityImpl(Uri uri, bool removeFromLibrary = true, bool useLocking = false)
|
||||
{
|
||||
HTTPCacheFileInfo info;
|
||||
bool inStats = library.TryGetValue(uri, out info);
|
||||
if (inStats)
|
||||
info.Delete();
|
||||
|
||||
if (inStats && removeFromLibrary)
|
||||
{
|
||||
if (useLocking)
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
library.Remove(uri);
|
||||
UsedIndexes.Remove(info.MappedNameIDX);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (useLocking)
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.SaveCacheLibrary));
|
||||
}
|
||||
|
||||
internal static bool IsCachedEntityExpiresInTheFuture(HTTPRequest request)
|
||||
{
|
||||
if (!IsSupported || request.DisableCache)
|
||||
return false;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
HTTPCacheFileInfo info = null;
|
||||
|
||||
using (new ReadLock(rwLock))
|
||||
{
|
||||
if (!library.TryGetValue(request.CurrentUri, out info))
|
||||
return false;
|
||||
}
|
||||
|
||||
return info.WillExpireInTheFuture(request.State == HTTPRequestStates.ConnectionTimedOut ||
|
||||
request.State == HTTPRequestStates.TimedOut ||
|
||||
request.State == HTTPRequestStates.Error ||
|
||||
(request.State == HTTPRequestStates.Finished && request.Response != null && request.Response.StatusCode >= 500));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility function to set the cache control headers according to the spec.: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
internal static void SetHeaders(HTTPRequest request)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
request.RemoveHeader("If-None-Match");
|
||||
request.RemoveHeader("If-Modified-Since");
|
||||
|
||||
HTTPCacheFileInfo info = null;
|
||||
|
||||
using (new ReadLock(rwLock))
|
||||
{
|
||||
if (!library.TryGetValue(request.CurrentUri, out info))
|
||||
return;
|
||||
}
|
||||
|
||||
info.SetUpRevalidationHeaders(request);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Functions
|
||||
|
||||
public static HTTPCacheFileInfo GetEntity(Uri uri)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return null;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
HTTPCacheFileInfo info = null;
|
||||
|
||||
using (new ReadLock(rwLock))
|
||||
library.TryGetValue(uri, out info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
internal static void GetFullResponse(HTTPRequest request)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
HTTPCacheFileInfo info = null;
|
||||
|
||||
using (new ReadLock(rwLock))
|
||||
{
|
||||
if (!library.TryGetValue(request.CurrentUri, out info))
|
||||
return;
|
||||
|
||||
info.ReadResponseTo(request);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storing
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given response can be cached. http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
|
||||
/// </summary>
|
||||
/// <returns>Returns true if cacheable, false otherwise.</returns>
|
||||
internal static bool IsCacheble(Uri uri, HTTPMethods method, HTTPResponse response)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return false;
|
||||
|
||||
if (method != HTTPMethods.Get)
|
||||
return false;
|
||||
|
||||
if (response == null)
|
||||
return false;
|
||||
|
||||
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.12 - Cache Replacement
|
||||
// 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.
|
||||
//if (response.StatusCode == 304)
|
||||
// return false;
|
||||
|
||||
// Partial response
|
||||
if (response.StatusCode == 206)
|
||||
return false;
|
||||
|
||||
if (response.StatusCode < 200 || response.StatusCode >= 400)
|
||||
return false;
|
||||
|
||||
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
|
||||
bool hasValidMaxAge = false;
|
||||
var cacheControls = response.GetHeaderValues("cache-control");
|
||||
if (cacheControls != null)
|
||||
{
|
||||
if (cacheControls.Exists(headerValue =>
|
||||
{
|
||||
HeaderParser parser = new HeaderParser(headerValue);
|
||||
if (parser.Values != null && parser.Values.Count > 0) {
|
||||
for (int i = 0; i < parser.Values.Count; ++i)
|
||||
{
|
||||
var value = parser.Values[i];
|
||||
|
||||
// https://csswizardry.com/2019/03/cache-control-for-civilians/#no-store
|
||||
if (value.Key == "no-store")
|
||||
return true;
|
||||
|
||||
if (value.Key == "max-age" && value.HasValue)
|
||||
{
|
||||
double maxAge;
|
||||
if (double.TryParse(value.Value, out maxAge))
|
||||
{
|
||||
// A negative max-age value is a no cache
|
||||
if (maxAge <= 0)
|
||||
return true;
|
||||
hasValidMaxAge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
|
||||
var pragmas = response.GetHeaderValues("pragma");
|
||||
if (pragmas != null)
|
||||
{
|
||||
if (pragmas.Exists(headerValue =>
|
||||
{
|
||||
string value = headerValue.ToLowerInvariant();
|
||||
return value.Contains("no-store") || value.Contains("no-cache");
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Responses with byte ranges not supported yet.
|
||||
var byteRanges = response.GetHeaderValues("content-range");
|
||||
if (byteRanges != null)
|
||||
return false;
|
||||
|
||||
// Store only if at least one caching header with proper value present
|
||||
|
||||
var etag = response.GetFirstHeaderValue("ETag");
|
||||
if (!string.IsNullOrEmpty(etag))
|
||||
return true;
|
||||
|
||||
var expires = response.GetFirstHeaderValue("Expires").ToDateTime(DateTime.FromBinary(0));
|
||||
if (expires >= DateTime.UtcNow)
|
||||
return true;
|
||||
|
||||
if (response.GetFirstHeaderValue("Last-Modified") != null)
|
||||
return true;
|
||||
|
||||
return hasValidMaxAge;
|
||||
}
|
||||
|
||||
internal static HTTPCacheFileInfo Store(Uri uri, HTTPMethods method, HTTPResponse response)
|
||||
{
|
||||
if (response == null || response.Data == null || response.Data.Length == 0)
|
||||
return null;
|
||||
|
||||
if (!IsSupported)
|
||||
return null;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
HTTPCacheFileInfo info = null;
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
if (!library.TryGetValue(uri, out info))
|
||||
{
|
||||
library.Add(uri, info = new HTTPCacheFileInfo(uri));
|
||||
UsedIndexes.Add(info.MappedNameIDX, info);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
info.Store(response);
|
||||
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("HTTPCacheService", string.Format("{0} - Saved to cache", uri.ToString()), response.baseRequest.Context);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If something happens while we write out the response, than we will delete it because it might be in an invalid state.
|
||||
DeleteEntityImpl(uri);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
internal static void SetUpCachingValues(Uri uri, HTTPResponse response)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
HTTPCacheFileInfo info = null;
|
||||
if (!library.TryGetValue(uri, out info))
|
||||
{
|
||||
library.Add(uri, info = new HTTPCacheFileInfo(uri));
|
||||
UsedIndexes.Add(info.MappedNameIDX, info);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
info.SetUpCachingValues(response);
|
||||
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Verbose("HTTPCacheService", string.Format("{0} - SetUpCachingValues done!", uri.ToString()), response.baseRequest.Context);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If something happens while we write out the response, than we will delete it because it might be in an invalid state.
|
||||
DeleteEntityImpl(uri);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static System.IO.Stream PrepareStreamed(Uri uri, HTTPResponse response)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return null;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
HTTPCacheFileInfo info;
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
if (!library.TryGetValue(uri, out info))
|
||||
{
|
||||
library.Add(uri, info = new HTTPCacheFileInfo(uri));
|
||||
UsedIndexes.Add(info.MappedNameIDX, info);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return info.GetSaveStream(response);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If something happens while we write out the response, than we will delete it because it might be in an invalid state.
|
||||
DeleteEntityImpl(uri, true, true);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Maintenance Functions
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all cache entity. Non blocking.
|
||||
/// <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>
|
||||
/// </summary>
|
||||
public static void BeginClear()
|
||||
{
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
if (InClearThread)
|
||||
return;
|
||||
InClearThread = true;
|
||||
|
||||
SetupCacheFolder();
|
||||
|
||||
PlatformSupport.Threading.ThreadedRunner.RunShortLiving(ClearImpl);
|
||||
}
|
||||
|
||||
private static void ClearImpl()
|
||||
{
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
try
|
||||
{
|
||||
// GetFiles will return a string array that contains the files in the folder with the full path
|
||||
string[] cacheEntries = HTTPManager.IOService.GetFiles(CacheFolder);
|
||||
|
||||
if (cacheEntries != null)
|
||||
for (int i = 0; i < cacheEntries.Length; ++i)
|
||||
{
|
||||
// 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.
|
||||
// So while there might be some problem with any file, we don't want to abort the whole for loop
|
||||
try
|
||||
{
|
||||
HTTPManager.IOService.FileDelete(cacheEntries[i]);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
UsedIndexes.Clear();
|
||||
library.Clear();
|
||||
NextNameIDX = 0x0001;
|
||||
|
||||
InClearThread = false;
|
||||
|
||||
PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.SaveCacheLibrary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all expired cache entity.
|
||||
/// <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>
|
||||
/// </summary>
|
||||
public static void BeginMaintainence(HTTPCacheMaintananceParams maintananceParam)
|
||||
{
|
||||
if (maintananceParam == null)
|
||||
throw new ArgumentNullException("maintananceParams == null");
|
||||
|
||||
if (!HTTPCacheService.IsSupported)
|
||||
return;
|
||||
|
||||
if (InMaintainenceThread)
|
||||
return;
|
||||
|
||||
InMaintainenceThread = true;
|
||||
|
||||
SetupCacheFolder();
|
||||
|
||||
PlatformSupport.Threading.ThreadedRunner.RunShortLiving(MaintananceImpl, maintananceParam);
|
||||
}
|
||||
|
||||
private static void MaintananceImpl(HTTPCacheMaintananceParams maintananceParam)
|
||||
{
|
||||
CheckSetup();
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Delete cache entries older than the given time.
|
||||
DateTime deleteOlderAccessed = DateTime.UtcNow - maintananceParam.DeleteOlder;
|
||||
List<HTTPCacheFileInfo> removedEntities = new List<HTTPCacheFileInfo>();
|
||||
foreach (var kvp in library)
|
||||
if (kvp.Value.LastAccess < deleteOlderAccessed)
|
||||
{
|
||||
DeleteEntityImpl(kvp.Key, false, false);
|
||||
removedEntities.Add(kvp.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < removedEntities.Count; ++i)
|
||||
{
|
||||
library.Remove(removedEntities[i].Uri);
|
||||
UsedIndexes.Remove(removedEntities[i].MappedNameIDX);
|
||||
}
|
||||
removedEntities.Clear();
|
||||
|
||||
ulong cacheSize = GetCacheSizeImpl();
|
||||
|
||||
// This step will delete all entries starting with the oldest LastAccess property while the cache size greater then the MaxCacheSize in the given param.
|
||||
if (cacheSize > maintananceParam.MaxCacheSize)
|
||||
{
|
||||
List<HTTPCacheFileInfo> fileInfos = new List<HTTPCacheFileInfo>(library.Count);
|
||||
|
||||
foreach (var kvp in library)
|
||||
fileInfos.Add(kvp.Value);
|
||||
|
||||
fileInfos.Sort();
|
||||
|
||||
int idx = 0;
|
||||
while (cacheSize >= maintananceParam.MaxCacheSize && idx < fileInfos.Count)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fi = fileInfos[idx];
|
||||
ulong length = (ulong)fi.BodyLength;
|
||||
|
||||
DeleteEntityImpl(fi.Uri);
|
||||
|
||||
cacheSize -= length;
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
finally
|
||||
{
|
||||
++idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
InMaintainenceThread = false;
|
||||
|
||||
PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.SaveCacheLibrary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetCacheEntityCount()
|
||||
{
|
||||
if (!HTTPCacheService.IsSupported)
|
||||
return 0;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
using (new ReadLock(rwLock))
|
||||
{
|
||||
return library.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public static ulong GetCacheSize()
|
||||
{
|
||||
if (!IsSupported)
|
||||
return 0;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
using (new ReadLock (rwLock))
|
||||
{
|
||||
return GetCacheSizeImpl();
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong GetCacheSizeImpl()
|
||||
{
|
||||
ulong size = 0;
|
||||
|
||||
foreach (var kvp in library)
|
||||
if (kvp.Value.BodyLength > 0)
|
||||
size += (ulong)kvp.Value.BodyLength;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cache Library Management
|
||||
|
||||
private static void LoadLibrary()
|
||||
{
|
||||
// Already loaded?
|
||||
if (library != null)
|
||||
return;
|
||||
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
int version = 1;
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
library = new Dictionary<Uri, HTTPCacheFileInfo>(new UriComparer());
|
||||
try
|
||||
{
|
||||
using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.OpenRead))
|
||||
using (var br = new System.IO.BinaryReader(fs))
|
||||
{
|
||||
version = br.ReadInt32();
|
||||
|
||||
if (version > 1)
|
||||
NextNameIDX = br.ReadUInt64();
|
||||
|
||||
int statCount = br.ReadInt32();
|
||||
|
||||
for (int i = 0; i < statCount; ++i)
|
||||
{
|
||||
Uri uri = new Uri(br.ReadString());
|
||||
|
||||
var entity = new HTTPCacheFileInfo(uri, br, version);
|
||||
if (entity.IsExists())
|
||||
{
|
||||
library.Add(uri, entity);
|
||||
|
||||
if (version > 1)
|
||||
UsedIndexes.Add(entity.MappedNameIDX, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Exception("HTTPCacheService", "LoadLibrary", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (version == 1)
|
||||
BeginClear();
|
||||
else
|
||||
DeleteUnusedFiles();
|
||||
}
|
||||
|
||||
internal static void SaveLibrary()
|
||||
{
|
||||
if (library == null)
|
||||
return;
|
||||
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.Create))
|
||||
using (var bw = new System.IO.BinaryWriter(fs))
|
||||
{
|
||||
bw.Write(LibraryVersion);
|
||||
bw.Write(NextNameIDX);
|
||||
|
||||
bw.Write(library.Count);
|
||||
foreach (var kvp in library)
|
||||
{
|
||||
bw.Write(kvp.Key.ToString());
|
||||
|
||||
kvp.Value.SaveTo(bw);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
|
||||
HTTPManager.Logger.Exception("HTTPCacheService", "SaveLibrary", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetBodyLength(Uri uri, int bodyLength)
|
||||
{
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
using (new WriteLock(rwLock))
|
||||
{
|
||||
HTTPCacheFileInfo fileInfo;
|
||||
if (library.TryGetValue(uri, out fileInfo))
|
||||
fileInfo.BodyLength = bodyLength;
|
||||
else
|
||||
{
|
||||
library.Add(uri, fileInfo = new HTTPCacheFileInfo(uri, DateTime.UtcNow, bodyLength));
|
||||
UsedIndexes.Add(fileInfo.MappedNameIDX, fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all files from the cache folder that isn't in the Library.
|
||||
/// </summary>
|
||||
private static void DeleteUnusedFiles()
|
||||
{
|
||||
if (!IsSupported)
|
||||
return;
|
||||
|
||||
CheckSetup();
|
||||
|
||||
// GetFiles will return a string array that contains the files in the folder with the full path
|
||||
string[] cacheEntries = HTTPManager.IOService.GetFiles(CacheFolder);
|
||||
|
||||
for (int i = 0; i < cacheEntries.Length; ++i)
|
||||
{
|
||||
// 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.
|
||||
// So while there might be some problem with any file, we don't want to abort the whole for loop
|
||||
try
|
||||
{
|
||||
string filename = System.IO.Path.GetFileName(cacheEntries[i]);
|
||||
UInt64 idx = 0;
|
||||
bool deleteFile = false;
|
||||
if (UInt64.TryParse(filename, System.Globalization.NumberStyles.AllowHexSpecifier, null, out idx))
|
||||
{
|
||||
using (new ReadLock(rwLock))
|
||||
deleteFile = !UsedIndexes.ContainsKey(idx);
|
||||
}
|
||||
else
|
||||
deleteFile = true;
|
||||
|
||||
if (deleteFile)
|
||||
HTTPManager.IOService.FileDelete(cacheEntries[i]);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d29e8551599393f4193bc4daa6968607
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user