Quick and dirty implementation of etag and cache control: Note: threading issues! Don't use as is
using System; | |
using System.IO; | |
using System.Net.Http; | |
using System.Security.Cryptography; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace HttpCacheClient | |
{ | |
public class CacheHttpHandler : DelegatingHandler | |
{ | |
private readonly string foldername; | |
public CacheHttpHandler(string foldername = "cache") : base(new SocketsHttpHandler()) | |
{ | |
this.foldername = foldername; | |
if (!Directory.Exists(foldername)) | |
Directory.CreateDirectory(foldername); | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
base.InnerHandler.Dispose(); | |
base.Dispose(disposing); | |
} | |
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |
{ | |
bool isCacheable = (request.Method == HttpMethod.Get || request.Method == HttpMethod.Head) && | |
request.Headers.Authorization == null && request.Content == null; | |
string etag = null; | |
string cachefile = null; | |
if (isCacheable) | |
{ | |
var hash = MD5Hash(request.RequestUri.OriginalString); | |
cachefile = Path.Combine(foldername, hash); | |
if (File.Exists(cachefile)) | |
{ | |
long expires; | |
using (var fs = File.OpenRead(cachefile +".dat")) | |
{ | |
using(var br = new BinaryReader(fs)) | |
{ | |
expires = br.ReadInt64(); | |
etag = br.ReadString(); | |
} | |
} | |
if (new DateTime(expires) > DateTime.UtcNow) | |
{ | |
var resp = new HttpResponseMessage(System.Net.HttpStatusCode.OK); | |
resp.Content = new StreamContent(File.OpenRead(cachefile)); | |
return resp; | |
} | |
if(string.IsNullOrEmpty(etag)) | |
{ | |
File.Delete(cachefile + ".dat"); | |
File.Delete(cachefile); | |
etag = null; | |
} | |
else | |
{ | |
request.Headers.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue(etag)); | |
} | |
} | |
} | |
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); | |
if (!string.IsNullOrEmpty(etag)) | |
{ | |
if (response.StatusCode == System.Net.HttpStatusCode.NotModified) | |
{ | |
WriteMetadata(response, cachefile); //update metadata | |
response.StatusCode = System.Net.HttpStatusCode.OK; | |
response.Content = new StreamContent(File.OpenRead(cachefile)); | |
return response; | |
} | |
else | |
{ | |
File.Delete(cachefile); | |
File.Delete(cachefile + ".dat"); | |
} | |
} | |
if (isCacheable && response.StatusCode == System.Net.HttpStatusCode.OK) | |
{ | |
bool canCache = response.Headers.ETag?.Tag != null || response.Headers.CacheControl != null && | |
!response.Headers.CacheControl.NoCache && | |
!response.Headers.CacheControl.NoStore && | |
response.Headers.CacheControl.Public; | |
if(canCache) | |
{ | |
using (var fs = File.Create(cachefile)) | |
{ | |
await response.Content.CopyToAsync(fs).ConfigureAwait(false); | |
response.Content.Dispose(); | |
} | |
WriteMetadata(response, cachefile); | |
response.Content = new StreamContent(File.OpenRead(cachefile)); | |
} | |
} | |
return response; | |
} | |
private static void WriteMetadata(HttpResponseMessage response, string file) | |
{ | |
using (var fs = File.Create(file +".dat")) | |
{ | |
using (BinaryWriter bw = new BinaryWriter(fs)) | |
{ | |
bw.Write(response.Headers.CacheControl.MaxAge.HasValue ? DateTime.UtcNow.Add(response.Headers.CacheControl.MaxAge.Value).Ticks : 0L); | |
bw.Write(response.Headers.ETag?.Tag ?? string.Empty); | |
} | |
} | |
} | |
private static MD5CryptoServiceProvider md5provider = new MD5CryptoServiceProvider(); | |
private static string MD5Hash(string input) | |
{ | |
StringBuilder hash = new StringBuilder(); | |
byte[] bytes = md5provider.ComputeHash(new UTF8Encoding().GetBytes(input)); | |
for (int i = 0; i < bytes.Length; i++) | |
{ | |
hash.Append(bytes[i].ToString("x2")); | |
} | |
return hash.ToString(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment