/// <inheritdoc/> public async Task <CachedBuffer> GetAsync(string key) { IFileInfo fileInfo = this.fileProvider.GetFileInfo(this.ToFilePath(key)); byte[] buffer; // Check to see if the file exists. if (!fileInfo.Exists) { return(default(CachedBuffer)); } long length; using (Stream stream = fileInfo.CreateReadStream()) { length = stream.Length; // Buffer is returned to the pool in the middleware buffer = BufferDataPool.Rent((int)length); await stream.ReadAsync(buffer, 0, (int)length); } return(new CachedBuffer(buffer, length)); }
private async Task SendResponse(ImageContext imageContext, string key, DateTimeOffset lastModified, byte[] buffer, int length) { imageContext.ComprehendRequestHeaders(lastModified, length); string contentType = FormatHelpers.GetContentType(this.options.Configuration, key); switch (imageContext.GetPreconditionState()) { case ImageContext.PreconditionState.Unspecified: case ImageContext.PreconditionState.ShouldProcess: if (imageContext.IsHeadRequest()) { await imageContext.SendStatusAsync(ResponseConstants.Status200Ok, contentType); } this.logger.LogImageServed(imageContext.GetDisplayUrl(), key); if (buffer == null) { // We're pulling the buffer from the cache. This should be pooled. CachedBuffer cachedBuffer = await this.cache.GetAsync(key); await imageContext.SendAsync(contentType, cachedBuffer.Buffer, cachedBuffer.Length); BufferDataPool.Return(cachedBuffer.Buffer); } else { await imageContext.SendAsync(contentType, buffer, length); } break; case ImageContext.PreconditionState.NotModified: this.logger.LogImageNotModified(imageContext.GetDisplayUrl()); await imageContext.SendStatusAsync(ResponseConstants.Status304NotModified, contentType); break; case ImageContext.PreconditionState.PreconditionFailed: this.logger.LogImagePreconditionFailed(imageContext.GetDisplayUrl()); await imageContext.SendStatusAsync(ResponseConstants.Status412PreconditionFailed, contentType); break; default: var exception = new NotImplementedException(imageContext.GetPreconditionState().ToString()); Debug.Fail(exception.ToString()); throw exception; } }
/// <inheritdoc/> public async Task <byte[]> ResolveImageAsync(HttpContext context, ILogger logger) { // Path has already been correctly parsed before here. IFileInfo fileInfo = this.fileProvider.GetFileInfo(context.Request.Path.Value); byte[] buffer; // Check to see if the file exists. if (!fileInfo.Exists) { return(null); } using (Stream stream = fileInfo.CreateReadStream()) { // Buffer is returned to the pool in the middleware buffer = BufferDataPool.Rent((int)stream.Length); await stream.ReadAsync(buffer, 0, (int)stream.Length); } return(buffer); }
/// <inheritdoc/> public async Task <byte[]> ResolveImageAsync(HttpContext context, ILogger logger) { // Path has already been correctly parsed before here. var file = await _mediaStore.MapFileAsync(context.Request.Path); byte[] buffer; // Check to see if the file exists. if (file == null) { return(null); } using (Stream stream = file.CreateReadStream()) { // Buffer is returned to the pool in the middleware buffer = BufferDataPool.Rent((int)stream.Length); await stream.ReadAsync(buffer, 0, (int)stream.Length); } return(buffer); }
/// <inheritdoc/> public async Task <byte[]> ResolveImageAsync(HttpContext context, ILogger logger) { // Path has already been correctly parsed before here. var filePath = _mediaStore.MapPublicUrlToPath(context.Request.PathBase + context.Request.Path.Value); var file = await _mediaStore.GetFileInfoAsync(filePath); // Check to see if the file exists. if (file == null) { return(null); } byte[] buffer; using (var stream = await _mediaStore.GetFileStreamAsync(filePath)) { // Buffer is returned to the pool in the middleware buffer = BufferDataPool.Rent((int)stream.Length); await stream.ReadAsync(buffer, 0, (int)stream.Length); } return(buffer); }
/// <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?.Invoke(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; } // Create a cache key based on all the components of the requested url string uri = $"{context.Request.Host.ToString().ToLowerInvariant()}/{context.Request.PathBase.ToString().ToLowerInvariant()}/{context.Request.Path}{QueryString.Create(commands)}"; string key = CacheHash.Create(uri, this.options.Configuration); // Prevent identical requests from running at the same time // This reduces the overheads of unnecessary processing plus avoids file locks bool processRequest = true; using (await this.asyncKeyLock.LockAsync(key)) { // 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 processRequest = false; } if (processRequest) { CachedInfo info = await this.cache.IsExpiredAsync(context, key, DateTime.UtcNow.AddDays(-this.options.MaxCacheDays)); var imageContext = new ImageContext(context, this.options); if (info.Equals(default(CachedInfo))) { // Cache has tried to resolve the source image and failed // Log the error but let the pipeline handle the 404 this.logger.LogImageResolveFailed(imageContext.GetDisplayUrl()); processRequest = false; } if (processRequest) { 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()); processRequest = false; } if (processRequest) { // 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); 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, 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) { // 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(); // Buffer should have been rented in IImageResolver BufferDataPool.Return(inBuffer); BufferDataPool.Return(outBuffer); } } } } if (!processRequest) { // Call the next delegate/middleware in the pipeline await this.next(context); } }
/// <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); } }