Skip to content

Commit f91c093

Browse files
Merge pull request #240 from SixLabors/js/lru-cache-fixes
Allow runtime physical deletion of cached images
2 parents 21a6bc1 + 8bfe51f commit f91c093

2 files changed

Lines changed: 52 additions & 24 deletions

File tree

src/ImageSharp.Web/Middleware/ImageContext.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,12 @@ internal enum PreconditionState
8686
}
8787

8888
/// <summary>
89-
/// Returns the current HTTP request display url.
89+
/// Returns the current HTTP image request display url.
9090
/// </summary>
91-
/// <returns>The. </returns>
91+
/// <returns>
92+
/// The combined components of the image request URL in a fully un-escaped form (except
93+
/// for the QueryString) suitable only for display.
94+
/// </returns>
9295
public string GetDisplayUrl() => this.request.GetDisplayUrl();
9396

9497
/// <summary>

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Linq;
1010
using System.Threading.Tasks;
1111
using Microsoft.AspNetCore.Http;
12+
using Microsoft.AspNetCore.Http.Extensions;
1213
using Microsoft.Extensions.Logging;
1314
using Microsoft.Extensions.Options;
1415
using Microsoft.IO;
@@ -184,12 +185,14 @@ public ImageSharpMiddleware(
184185
/// <summary>
185186
/// Performs operations upon the current request.
186187
/// </summary>
187-
/// <param name="context">The current HTTP request context.</param>
188+
/// <param name="httpContext">The current HTTP request context.</param>
188189
/// <returns>The <see cref="Task"/>.</returns>
189-
public async Task Invoke(HttpContext context)
190+
public Task Invoke(HttpContext httpContext) => this.Invoke(httpContext, false);
191+
192+
private async Task Invoke(HttpContext httpContext, bool retry)
190193
{
191194
// We expect to get concrete collection type which removes virtual dispatch concerns and enumerator allocations
192-
CommandCollection commands = this.requestParser.ParseRequestCommands(context);
195+
CommandCollection commands = this.requestParser.ParseRequestCommands(httpContext);
193196

194197
if (commands.Count > 0)
195198
{
@@ -209,44 +212,44 @@ public async Task Invoke(HttpContext context)
209212
}
210213

211214
await this.options.OnParseCommandsAsync.Invoke(
212-
new ImageCommandContext(context, commands, this.commandParser, this.parserCulture));
215+
new ImageCommandContext(httpContext, commands, this.commandParser, this.parserCulture));
213216

214217
// Get the correct service for the request
215218
IImageProvider provider = null;
216219
foreach (IImageProvider resolver in this.providers)
217220
{
218-
if (resolver.Match(context))
221+
if (resolver.Match(httpContext))
219222
{
220223
provider = resolver;
221224
break;
222225
}
223226
}
224227

225228
if ((commands.Count == 0 && provider?.ProcessingBehavior != ProcessingBehavior.All)
226-
|| provider?.IsValidRequest(context) != true)
229+
|| provider?.IsValidRequest(httpContext) != true)
227230
{
228231
// Nothing to do. call the next delegate/middleware in the pipeline
229-
await this.next(context);
232+
await this.next(httpContext);
230233
return;
231234
}
232235

233-
IImageResolver sourceImageResolver = await provider.GetAsync(context);
236+
IImageResolver sourceImageResolver = await provider.GetAsync(httpContext);
234237

235238
if (sourceImageResolver is null)
236239
{
237240
// Log the error but let the pipeline handle the 404
238241
// by calling the next delegate/middleware in the pipeline.
239-
var imageContext = new ImageContext(context, this.options);
240-
this.logger.LogImageResolveFailed(imageContext.GetDisplayUrl());
241-
await this.next(context);
242+
this.logger.LogImageResolveFailed(httpContext.Request.GetDisplayUrl());
243+
await this.next(httpContext);
242244
return;
243245
}
244246

245247
await this.ProcessRequestAsync(
246-
context,
248+
httpContext,
247249
sourceImageResolver,
248-
new ImageContext(context, this.options),
249-
commands);
250+
new ImageContext(httpContext, this.options),
251+
commands,
252+
retry);
250253
}
251254

252255
private void StripUnknownCommands(CommandCollection commands, int startAtIndex)
@@ -263,14 +266,15 @@ private void StripUnknownCommands(CommandCollection commands, int startAtIndex)
263266
}
264267

265268
private async Task ProcessRequestAsync(
266-
HttpContext context,
269+
HttpContext httpContext,
267270
IImageResolver sourceImageResolver,
268271
ImageContext imageContext,
269-
CommandCollection commands)
272+
CommandCollection commands,
273+
bool retry)
270274
{
271275
// Create a hashed cache key
272276
string key = this.cacheHash.Create(
273-
this.cacheKey.Create(context, commands),
277+
this.cacheKey.Create(httpContext, commands),
274278
this.options.CacheHashLength);
275279

276280
// Check the cache, if present, not out of date and not requiring an update
@@ -283,7 +287,7 @@ private async Task ProcessRequestAsync(
283287

284288
if (!readResult.IsNewOrUpdated)
285289
{
286-
await this.SendResponseAsync(imageContext, key, readResult.CacheImageMetadata, readResult.Resolver, null);
290+
await this.SendResponseAsync(httpContext, imageContext, key, readResult.CacheImageMetadata, readResult.Resolver, null, retry);
287291
return;
288292
}
289293

@@ -379,7 +383,7 @@ private async Task ProcessRequestAsync(
379383
outStream.Position = 0;
380384
string contentType = format.DefaultMimeType;
381385
string extension = this.formatUtilities.GetExtensionFromContentType(contentType);
382-
await this.options.OnProcessedAsync.Invoke(new ImageProcessingContext(context, outStream, commands, contentType, extension));
386+
await this.options.OnProcessedAsync.Invoke(new ImageProcessingContext(httpContext, outStream, commands, contentType, extension));
383387
outStream.Position = 0;
384388

385389
cachedImageMetadata = new ImageCacheMetadata(
@@ -409,7 +413,7 @@ private async Task ProcessRequestAsync(
409413
}
410414
}
411415

412-
await this.SendResponseAsync(imageContext, key, readResult.CacheImageMetadata, readResult.Resolver, outStream);
416+
await this.SendResponseAsync(httpContext, imageContext, key, readResult.CacheImageMetadata, readResult.Resolver, outStream, retry);
413417
}
414418
finally
415419
{
@@ -490,11 +494,13 @@ private async Task<ImageWorkerResult> IsNewOrUpdatedAsync(
490494
}
491495

492496
private async Task SendResponseAsync(
497+
HttpContext httpContext,
493498
ImageContext imageContext,
494499
string key,
495500
ImageCacheMetadata metadata,
496501
IImageCacheResolver cacheResolver,
497-
Stream stream)
502+
Stream stream,
503+
bool retry)
498504
{
499505
imageContext.ComprehendRequestHeaders(metadata.CacheLastWriteTimeUtc, metadata.ContentLength);
500506

@@ -518,10 +524,29 @@ private async Task SendResponseAsync(
518524
}
519525
else
520526
{
521-
using (Stream cacheStream = await cacheResolver.OpenReadAsync())
527+
try
522528
{
529+
using Stream cacheStream = await cacheResolver.OpenReadAsync();
523530
await imageContext.SendAsync(cacheStream, metadata);
524531
}
532+
catch (Exception ex)
533+
{
534+
if (!retry)
535+
{
536+
// The image has failed to be returned from the cache.
537+
// This can happen if the cached image has been physically deleted but the item is still in the LRU cache.
538+
// We'll retry running the request again in it's entirety. This ensures any changes to the source are tracked also.
539+
CacheResolverLru.TryRemove(key);
540+
await this.Invoke(httpContext);
541+
return;
542+
}
543+
544+
// We've already tried to run this request before.
545+
// Log the error internally then rethrow.
546+
// We don't call next here, the pipeline will automatically handle it
547+
this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex);
548+
throw;
549+
}
525550
}
526551

527552
return;

0 commit comments

Comments
 (0)