/// <summary> /// Performs operations upon the current request. /// </summary> /// <param name="context">The current HTTP request context.</param> /// <returns>The <see cref="Task"/>.</returns> public async Task Invoke(HttpContext context) { IDictionary <string, string> commands = this.requestParser.ParseRequestCommands(context) .Where(kvp => this.knownCommands.Contains(kvp.Key)) .ToDictionary(p => p.Key, p => p.Value); this.options.OnParseCommands?.Invoke(new ImageCommandContext(context, commands, CommandParser.Instance)); // Get the correct service for the request. IImageProvider provider = this.resolvers.FirstOrDefault(r => r.Match(context)); if (provider?.IsValidRequest(context) != true) { // Nothing to do. call the next delegate/middleware in the pipeline await this.next(context).ConfigureAwait(false); return; } // Create a cache key based on all the components of the requested url string uri = GetUri(context, commands); string key = this.cacheHash.Create(uri, this.options.CachedNameLength); bool processRequest = true; var imageContext = new ImageContext(context, this.options); IImageResolver sourceImageResolver = await provider.GetAsync(context).ConfigureAwait(false); if (sourceImageResolver == null) { // Log the error but let the pipeline handle the 404 this.logger.LogImageResolveFailed(imageContext.GetDisplayUrl()); processRequest = false; } ImageMetaData sourceImageMetadata = default; if (processRequest) { // Lock any reads when a write is being done for the same key to prevent potential file locks. using (await AsyncLock.ReaderLockAsync(key).ConfigureAwait(false)) { // Check to see if the cache contains this image sourceImageMetadata = await sourceImageResolver.GetMetaDataAsync().ConfigureAwait(false); IImageResolver cachedImageResolver = await this.cache.GetAsync(key).ConfigureAwait(false); if (cachedImageResolver != null) { ImageMetaData cachedImageMetadata = await cachedImageResolver.GetMetaDataAsync().ConfigureAwait(false); if (cachedImageMetadata != default) { // Has the cached image expired or has the source image been updated? if (cachedImageMetadata.LastWriteTimeUtc > sourceImageMetadata.LastWriteTimeUtc && cachedImageMetadata.LastWriteTimeUtc > DateTimeOffset.Now.AddDays(-this.options.MaxCacheDays)) { // We're pulling the image from the cache. using (Stream cachedBuffer = await cachedImageResolver.OpenReadAsync().ConfigureAwait(false)) { await this.SendResponse(imageContext, key, cachedBuffer, cachedImageMetadata).ConfigureAwait(false); } return; } } } } // Not cached? Let's get it from the image resolver. ChunkedMemoryStream outStream = null; try { if (processRequest) { // Enter a write lock which locks writing and any reads for the same request. // This reduces the overheads of unnecessary processing plus avoids file locks. using (await AsyncLock.WriterLockAsync(key).ConfigureAwait(false)) { // No allocations here for inStream since we are passing the raw input stream. // outStream allocation depends on the memory allocator used. ImageMetaData cachedImageMetadata = default; outStream = new ChunkedMemoryStream(this.memoryAllocator); using (Stream inStream = await sourceImageResolver.OpenReadAsync().ConfigureAwait(false)) using (var image = FormattedImage.Load(this.options.Configuration, inStream)) { image.Process(this.logger, this.processors, commands); this.options.OnBeforeSave?.Invoke(image); image.Save(outStream); // Check to see if the source metadata has a cachecontrol max-age value and use it to // override the default max age from our options. var maxAge = TimeSpan.FromDays(this.options.MaxBrowserCacheDays); if (!sourceImageMetadata.CacheControlMaxAge.Equals(TimeSpan.MinValue)) { maxAge = sourceImageMetadata.CacheControlMaxAge; } cachedImageMetadata = new ImageMetaData(DateTime.UtcNow, image.Format.DefaultMimeType, maxAge); } // Allow for any further optimization of the image. Always reset the position just in case. outStream.Position = 0; string contentType = cachedImageMetadata.ContentType; string extension = this.formatUtilities.GetExtensionFromContentType(contentType); this.options.OnProcessed?.Invoke(new ImageProcessingContext(context, outStream, commands, contentType, extension)); outStream.Position = 0; // Save the image to the cache and send the response to the caller. await this.cache.SetAsync(key, outStream, cachedImageMetadata).ConfigureAwait(false); await this.SendResponse(imageContext, key, outStream, cachedImageMetadata).ConfigureAwait(false); } } } catch (Exception ex) { // Log the error internally then rethrow. // We don't call next here, the pipeline will automatically handle it this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex); throw; } finally { outStream?.Dispose(); } } if (!processRequest) { // Call the next delegate/middleware in the pipeline await this.next(context).ConfigureAwait(false); } }
private async Task ProcessRequestAsync(HttpContext context, bool processRequest, IImageResolver sourceImageResolver, ImageContext imageContext, IDictionary <string, string> commands) { // Create a cache key based on all the components of the requested url string uri = GetUri(context, commands); string key = this.cacheHash.Create(uri, this.options.CachedNameLength); ImageMetadata sourceImageMetadata = default; if (processRequest) { // Lock any reads when a write is being done for the same key to prevent potential file locks. using (await AsyncLock.ReaderLockAsync(key)) { // Check to see if the cache contains this image sourceImageMetadata = await sourceImageResolver.GetMetaDataAsync(); IImageCacheResolver cachedImageResolver = await this.cache.GetAsync(key); if (cachedImageResolver != null) { ImageCacheMetadata cachedImageMetadata = await cachedImageResolver.GetMetaDataAsync(); if (cachedImageMetadata != default) { // Has the cached image expired or has the source image been updated? if (cachedImageMetadata.SourceLastWriteTimeUtc == sourceImageMetadata.LastWriteTimeUtc && cachedImageMetadata.CacheLastWriteTimeUtc > DateTimeOffset.Now.AddDays(-this.options.MaxCacheDays)) { // We're pulling the image from the cache. using (Stream cachedBuffer = await cachedImageResolver.OpenReadAsync()) { await this.SendResponseAsync(imageContext, key, cachedBuffer, cachedImageMetadata); } return; } } } } // Not cached? Let's get it from the image resolver. ChunkedMemoryStream outStream = null; try { if (processRequest) { // Enter a write lock which locks writing and any reads for the same request. // This reduces the overheads of unnecessary processing plus avoids file locks. using (await AsyncLock.WriterLockAsync(key)) { // No allocations here for inStream since we are passing the raw input stream. // outStream allocation depends on the memory allocator used. ImageCacheMetadata cachedImageMetadata = default; outStream = new ChunkedMemoryStream(); using (Stream inStream = await sourceImageResolver.OpenReadAsync()) { IImageFormat format; // No commands? We simply copy the stream across. if (commands.Count == 0) { format = Image.DetectFormat(this.options.Configuration, inStream); await inStream.CopyToAsync(outStream); } else { using (var image = FormattedImage.Load(this.options.Configuration, inStream)) { image.Process(this.logger, this.processors, commands); this.options.OnBeforeSave?.Invoke(image); image.Save(outStream); format = image.Format; } } // Check to see if the source metadata has a cachecontrol max-age value and use it to // override the default max age from our options. var maxAge = TimeSpan.FromDays(this.options.MaxBrowserCacheDays); if (!sourceImageMetadata.CacheControlMaxAge.Equals(TimeSpan.MinValue)) { maxAge = sourceImageMetadata.CacheControlMaxAge; } cachedImageMetadata = new ImageCacheMetadata( sourceImageMetadata.LastWriteTimeUtc, DateTime.UtcNow, format.DefaultMimeType, maxAge); } // Allow for any further optimization of the image. Always reset the position just in case. outStream.Position = 0; string contentType = cachedImageMetadata.ContentType; string extension = this.formatUtilities.GetExtensionFromContentType(contentType); this.options.OnProcessed?.Invoke(new ImageProcessingContext(context, outStream, commands, contentType, extension)); outStream.Position = 0; // Save the image to the cache and send the response to the caller. await this.cache.SetAsync(key, outStream, cachedImageMetadata); await this.SendResponseAsync(imageContext, key, outStream, cachedImageMetadata); } } } catch (Exception ex) { // Log the error internally then rethrow. // We don't call next here, the pipeline will automatically handle it this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex); throw; } finally { outStream?.Dispose(); } } }
private async Task ProcessRequestAsync( HttpContext context, IImageResolver sourceImageResolver, ImageContext imageContext, IDictionary <string, string> commands) { // Create a cache key based on all the components of the requested url string uri = GetUri(context, commands); string key = this.cacheHash.Create(uri, this.options.CachedNameLength); // Check the cache, if present, not out of date and not requiring and update // we'll simply serve the file from there. (bool newOrUpdated, ImageMetadata sourceImageMetadata) = await this.IsNewOrUpdatedAsync(sourceImageResolver, imageContext, key); if (!newOrUpdated) { return; } // Not cached? Let's get it from the image resolver. RecyclableMemoryStream outStream = null; // Enter a write lock which locks writing and any reads for the same request. // This reduces the overheads of unnecessary processing plus avoids file locks. await WriteWorkers.GetOrAdd( key, _ => new Lazy <Task>( async() => { try { // Prevent a second request from starting a read during write execution. if (ReadWorkers.TryGetValue(key, out Lazy <Task <(bool, ImageMetadata)> > readWork)) { await readWork.Value; } ImageCacheMetadata cachedImageMetadata = default; outStream = new RecyclableMemoryStream(this.options.MemoryStreamManager); IImageFormat format; // 14.9.3 CacheControl Max-Age // Check to see if the source metadata has a CacheControl Max-Age value // and use it to override the default max age from our options. TimeSpan maxAge = this.options.BrowserMaxAge; if (!sourceImageMetadata.CacheControlMaxAge.Equals(TimeSpan.MinValue)) { maxAge = sourceImageMetadata.CacheControlMaxAge; } using (Stream inStream = await sourceImageResolver.OpenReadAsync()) { // No commands? We simply copy the stream across. if (commands.Count == 0) { await inStream.CopyToAsync(outStream); outStream.Position = 0; format = await Image.DetectFormatAsync(this.options.Configuration, outStream); } else { using var image = FormattedImage.Load(this.options.Configuration, inStream); image.Process( this.logger, this.processors, commands, this.commandParser, this.parserCulture); await this.options.OnBeforeSaveAsync.Invoke(image); image.Save(outStream); format = image.Format; } } // Allow for any further optimization of the image. outStream.Position = 0; string contentType = format.DefaultMimeType; string extension = this.formatUtilities.GetExtensionFromContentType(contentType); await this.options.OnProcessedAsync.Invoke(new ImageProcessingContext(context, outStream, commands, contentType, extension)); outStream.Position = 0; cachedImageMetadata = new ImageCacheMetadata( sourceImageMetadata.LastWriteTimeUtc, DateTime.UtcNow, contentType, maxAge, outStream.Length); // Save the image to the cache and send the response to the caller. await this.cache.SetAsync(key, outStream, cachedImageMetadata); // Remove the resolver from the cache so we always resolve next request // for the same key. CacheResolverLru.TryRemove(key); await this.SendResponseAsync(imageContext, key, cachedImageMetadata, outStream, null); } catch (Exception ex) { // Log the error internally then rethrow. // We don't call next here, the pipeline will automatically handle it this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex); throw; } finally { await this.StreamDisposeAsync(outStream); WriteWorkers.TryRemove(key, out Lazy <Task> _); } }, LazyThreadSafetyMode.ExecutionAndPublication)).Value; }
/// <summary> /// Performs operations upon the current request. /// </summary> /// <param name="context">The current HTTP request context.</param> /// <returns>The <see cref="Task"/>.</returns> public async Task Invoke(HttpContext context) { IDictionary <string, string> commands = this.requestParser.ParseRequestCommands(context) .Where(kvp => this.knownCommands.Contains(kvp.Key)) .ToDictionary(p => p.Key, p => p.Value); this.options.OnParseCommands?.Invoke(new ImageCommandContext(context, commands, CommandParser.Instance)); // Get the correct service for the request. IImageProvider provider = this.resolvers.FirstOrDefault(r => r.Match(context)); if (provider?.IsValidRequest(context) != true) { // Nothing to do. call the next delegate/middleware in the pipeline await this.next(context).ConfigureAwait(false); return; } // Create a cache key based on all the components of the requested url string uri = GetUri(context, commands); string key = this.cacheHash.Create(uri, this.options.CachedNameLength); bool processRequest = true; var imageContext = new ImageContext(context, this.options); IImageResolver resolvedImage = provider.Get(context); if (resolvedImage == null) { // Log the error but let the pipeline handle the 404 this.logger.LogImageResolveFailed(imageContext.GetDisplayUrl()); processRequest = false; } if (processRequest) { // Lock any reads when a write is being done for the same key to prevent potential file locks. using (await AsyncLock.ReaderLockAsync(key).ConfigureAwait(false)) { DateTime lastWriteTimeUtc = await resolvedImage.GetLastWriteTimeUtcAsync().ConfigureAwait(false); CachedInfo info = await this.cache.IsExpiredAsync(context, key, lastWriteTimeUtc, DateTime.UtcNow.AddDays(-this.options.MaxCacheDays)).ConfigureAwait(false); if (!info.Expired) { // We're pulling the image from the cache. IImageResolver cachedImage = this.cache.Get(key); using (Stream cachedBuffer = await cachedImage.OpenReadAsync().ConfigureAwait(false)) { // Image is a cached image. Return the correct response now. await this.SendResponse(imageContext, key, info.LastModifiedUtc, cachedBuffer).ConfigureAwait(false); } return; } } // Not cached? Let's get it from the image resolver. ChunkedMemoryStream outStream = null; try { if (processRequest) { // Enter a write lock which locks writing and any reads for the same request. // This reduces the overheads of unnecessary processing plus avoids file locks. using (await AsyncLock.WriterLockAsync(key).ConfigureAwait(false)) { // No allocations here for inStream since we are passing the raw input stream. // outStream allocation depends on the memory allocator used. outStream = new ChunkedMemoryStream(this.memoryAllocator); using (Stream inStream = await resolvedImage.OpenReadAsync().ConfigureAwait(false)) using (var image = FormattedImage.Load(this.options.Configuration, inStream)) { image.Process(this.logger, this.processors, commands); this.options.OnBeforeSave?.Invoke(image); image.Save(outStream); } // Allow for any further optimization of the image. Always reset the position just in case. outStream.Position = 0; this.options.OnProcessed?.Invoke(new ImageProcessingContext(context, outStream, commands, Path.GetExtension(key))); outStream.Position = 0; DateTimeOffset cachedDate = await this.cache.SetAsync(key, outStream).ConfigureAwait(false); await this.SendResponse(imageContext, key, cachedDate, outStream).ConfigureAwait(false); } } } catch (Exception ex) { // Log the error internally then rethrow. // We don't call next here, the pipeline will automatically handle it this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex); throw; } finally { outStream?.Dispose(); } } if (!processRequest) { // Call the next delegate/middleware in the pipeline await this.next(context).ConfigureAwait(false); } }
/// <summary> /// Performs operations upon the current request. /// </summary> /// <param name="context">The current HTTP request context</param> /// <returns>The <see cref="Task"/></returns> public async Task Invoke(HttpContext context) { IDictionary <string, string> commands = this.uriParser.ParseUriCommands(context); this.options.OnValidate(new ImageValidationContext(context, commands, CommandParser.Instance)); if (!commands.Any() || !commands.Keys.Intersect(this.knownCommands).Any()) { // Nothing to do. call the next delegate/middleware in the pipeline await this.next(context); return; } // Get the correct service for the request. IImageResolver resolver = this.resolvers.FirstOrDefault(r => r.Match(context)); if (resolver == null || !await resolver.IsValidRequestAsync(context, this.logger)) { // Nothing to do. call the next delegate/middleware in the pipeline await this.next(context); return; } string uri = context.Request.Path + QueryString.Create(commands); string key = CacheHash.Create(uri, this.options.Configuration); CachedInfo info = await this.cache.IsExpiredAsync(key, DateTime.UtcNow.AddDays(-this.options.MaxCacheDays)); var imageContext = new ImageContext(context, this.options); if (!info.Expired) { // Image is a cached image. Return the correct response now. await this.SendResponse(imageContext, key, info.LastModifiedUtc, null, (int)info.Length); return; } // Not cached? Let's get it from the image resolver. byte[] inBuffer = null; byte[] outBuffer = null; MemoryStream outStream = null; try { inBuffer = await resolver.ResolveImageAsync(context, this.logger); if (inBuffer == null || inBuffer.Length == 0) { // Log the error but let the pipeline handle the 404 this.logger.LogImageResolveFailed(imageContext.GetDisplayUrl()); await this.next(context); return; } // No allocations here for inStream since we are passing the buffer. // TODO: How to prevent the allocation in outStream? Passing a pooled buffer won't let stream grow if needed. outStream = new MemoryStream(); using (var image = FormattedImage.Load(this.options.Configuration, inBuffer)) { image.Process(this.logger, this.processors, commands); image.Save(outStream); } // Allow for any further optimization of the image. Always reset the position just in case. outStream.Position = 0; this.options.OnProcessed(new ImageProcessingContext(context, outStream, Path.GetExtension(key))); outStream.Position = 0; int outLength = (int)outStream.Length; // Copy the outstream to the pooled buffer. outBuffer = BufferDataPool.Rent(outLength); await outStream.ReadAsync(outBuffer, 0, outLength); DateTimeOffset cachedDate = await this.cache.SetAsync(key, outBuffer, outLength); await this.SendResponse(imageContext, key, cachedDate, outBuffer, outLength); } catch (Exception ex) { this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex); } finally { outStream?.Dispose(); // Buffer should have been rented in IImageResolver BufferDataPool.Return(inBuffer); BufferDataPool.Return(outBuffer); } }