Skip to content

Commit

Permalink
Add ETag support to HybridCache
Browse files Browse the repository at this point in the history
  • Loading branch information
lilith committed Aug 26, 2022
1 parent 4273486 commit d6edf25
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 23 deletions.
69 changes: 55 additions & 14 deletions core/NewModuleHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -9,6 +10,7 @@
using System.Web;
using ImageResizer.Configuration.Performance;
using ImageResizer.Plugins;
using ImageResizer.Util;
using Microsoft.Extensions.Logging;
using Imazen.Common.Extensibility.StreamCache;

Expand Down Expand Up @@ -76,27 +78,66 @@ internal static async Task ProxyToStream(Stream sourceStream, HttpResponse respo

private void SetCachingHeaders(HttpContext context, string etag)
{
// var mins = c.get("clientcache.minutes", -1);
// //Set the expires value if present
// if (mins > 0)
// e.ResponseHeaders.Expires = DateTime.UtcNow.AddMinutes(mins);
//
// //NDJ Jan-16-2013. The last modified date sent in the headers should NOT match the source modified date when using DiskCaching.
// //Setting this will prevent 304s from being sent properly.
// // (Moved to NoCache)
//
// //Authenticated requests only allow caching on the client.
// //Anonymous requests get caching on the server, proxy and client
// if (context.Request.IsAuthenticated)
// e.ResponseHeaders.CacheControl = HttpCacheability.Private;
// else
// e.ResponseHeaders.CacheControl = HttpCacheability.Public;
//

context.Response.Headers["ETag"] = etag;

//TODO: Add support for max-age and public/private based on authentication.

//context.Response.CacheControl = "max-age=604800";
// if (options.DefaultCacheControlString != null)
// context.Response.Headers["Cache-Control"] = options.DefaultCacheControlString;
}

private async Task ServeFileFromDisk(HttpContext context, string path, string etag)
// private async Task ServeFileFromDisk(HttpContext context, string path, string etag)
// {
// using (var readStream = File.OpenRead(path))
// {
// if (readStream.Length < 1)
// {
// throw new InvalidOperationException("DiskCache file entry has zero bytes");
// }
//
// SetCachingHeaders(context, etag);
// await ProxyToStream(readStream, context.Response).ConfigureAwait(false);
// }
// }
//
public async Task ProcessWithStreamCache(ILogger logger, IStreamCache streamCache, HttpContext context, IAsyncResponsePlan plan)
{
using (var readStream = File.OpenRead(path))
{
if (readStream.Length < 1)
{
throw new InvalidOperationException("DiskCache file entry has zero bytes");
}

var cacheHash = PathUtils.Base64Hash(plan.RequestCachingKey);
var cacheHashQuoted = "\"" + cacheHash + "\"";
var cacheHashWeakValidation = "W/\"" + cacheHash + "\"";

SetCachingHeaders(context, etag);
await ProxyToStream(readStream, context.Response).ConfigureAwait(false);
// Send 304
var ifNoneMatch = (IList)context.Request.Headers.GetValues("If-None-Match");
if (ifNoneMatch != null && (ifNoneMatch.Contains(cacheHash) || ifNoneMatch.Contains(cacheHashQuoted) || ifNoneMatch.Contains(cacheHashWeakValidation)))
{
context.Response.StatusCode = 304;
context.Response.AppendHeader("Content-Length", "0");
context.Response.End();
return;
}
}

public async Task ProcessWithStreamCache(ILogger logger, IStreamCache streamCache, HttpContext context, IAsyncResponsePlan plan)
{
//
// context.Response.AppendHeader("ETag", cacheHashWeakValidation);
//
var keyBytes = System.Text.Encoding.UTF8.GetBytes(plan.RequestCachingKey);
var typeName = streamCache.GetType().Name;

Expand Down Expand Up @@ -143,7 +184,7 @@ public async Task ProcessWithStreamCache(ILogger logger, IStreamCache streamCach
{
throw new InvalidOperationException($"{typeName} returned cache entry with zero bytes");
}
SetCachingHeaders(context, plan.RequestCachingKey);
SetCachingHeaders(context, cacheHashWeakValidation);
await ProxyToStream(cacheResult.Data, context.Response).ConfigureAwait(true);
}
// logger?.LogDebug("Serving from {CacheName} {VirtualPath}?{CommandString}", typeName, plan., plan.RewrittenQuerystring);
Expand Down
5 changes: 4 additions & 1 deletion core/TODO_V5.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@

## TODO NOW for v5

* Add etag support, good default cache control, optimize file serving with and without cache installed.
* Add etag support for no cache installed
* Add better cache control configuration that works in async mode.


## TODO LATER for v5

Expand All @@ -19,6 +21,7 @@
* Unify storage providers and add support for proxying static non-image files
* Add support for caching source blobs under certain circumstances
* Figure out ideal &cache=no/disk/mem behavior
* HybridCache should stream directly from virtual file if the virtual file claims to be low-latency/overhead

## LATER

Expand Down
18 changes: 18 additions & 0 deletions core/Util/PathUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.Reflection;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -63,6 +64,23 @@ public static string SetExtension(string path, string newExtension)
path.Substring(query);
}

internal static string Base64Hash(string data)
{
using (var sha2 = SHA256.Create())
{
var stringBytes = Encoding.UTF8.GetBytes(data);
// check cache and return if cached
var hashBytes =
sha2.ComputeHash(stringBytes);
return Convert.ToBase64String(hashBytes)
.Replace("=", string.Empty)
.Replace('+', '-')
.Replace('/', '_');
}
}



/// <summary>
/// Removes all extension segments from the filename or URL, leaving the querystring intact. I.e, image.jpg.bmp.tiff?hi
/// would be image?hi
Expand Down
17 changes: 9 additions & 8 deletions plugins/ImageResizer.Plugins.HybridCache/HybridCachePlugin.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Remoting.Contexts;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using ImageResizer.Configuration;
using ImageResizer.Configuration.Performance;
using ImageResizer.Util;
using Imazen.Common.Extensibility.StreamCache;
using Imazen.Common.Instrumentation.Support.InfoAccumulators;
Expand Down Expand Up @@ -152,18 +155,16 @@ public bool CanProcess(HttpContext current, IAsyncResponsePlan e)
//TODO: use normal access instead of casting
return ((ResizeSettings)e.RewrittenQuerystring).Cache != ServerCacheMode.No;
}
public async Task ProcessAsync(HttpContext current, IAsyncResponsePlan plan)


public async Task ProcessAsync(HttpContext context, IAsyncResponsePlan plan)
{
if (!_isReady) throw new InvalidOperationException("HybridCache is not running");

//TODO: check etags, send not-modified as needed

//TODO: stream directly from virtual file if the virtual file claims to be low-latency/overhead

//TODO: Otherwise use GetOrCreateBytes



// And respond using the stream
await new NewModuleHelpers().ProcessWithStreamCache(_logger, this._cache, current, plan);
await new NewModuleHelpers().ProcessWithStreamCache(_logger, this._cache, context, plan);

}

Expand Down

0 comments on commit d6edf25

Please sign in to comment.