private async Task ProcessWithNoCache(HttpContext context, ImageJobInfo info) { // If we're not caching, we should always use the modified date from source blobs as part of the etag var betterCacheKey = await info.GetExactCacheKey(); if (context.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var etag) && betterCacheKey == etag) { context.Response.StatusCode = StatusCodes.Status304NotModified; context.Response.ContentLength = 0; context.Response.ContentType = null; return; } if (info.HasParams) { logger?.LogInformation($"Processing image {info.FinalVirtualPath} with params {info.CommandString}"); var imageData = await info.ProcessUncached(); var imageBytes = imageData.ResultBytes; var contentType = imageData.ContentType; // write to stream context.Response.ContentType = contentType; context.Response.ContentLength = imageBytes.Count; SetCachingHeaders(context, betterCacheKey); await context.Response.Body.WriteAsync(imageBytes.Array, imageBytes.Offset, imageBytes.Count); } else { logger?.LogInformation($"Proxying image {info.FinalVirtualPath} with params {info.CommandString}"); var contentType = PathHelpers.ContentTypeForImageExtension(info.EstimatedFileExtension); await using var sourceStream = (await info.GetPrimaryBlob()).OpenRead(); if (sourceStream.CanSeek) { if (sourceStream.Length == 0) { throw new InvalidOperationException("Source blob has zero bytes."); } context.Response.ContentType = contentType; context.Response.ContentLength = sourceStream.Length; SetCachingHeaders(context, betterCacheKey); await sourceStream.CopyToAsync(context.Response.Body); } else { context.Response.ContentType = contentType; SetCachingHeaders(context, betterCacheKey); await sourceStream.CopyToAsync(context.Response.Body); } } }
private async Task ProcessWithNoCache(HttpContext context, ImageJobInfo info) { // If we're not caching, we should always use the modified date from source blobs as part of the etag var betterCacheKey = await info.GetExactCacheKey(); if (context.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var etag) && betterCacheKey == etag) { GlobalPerf.Singleton.IncrementCounter("etag_hit"); context.Response.StatusCode = StatusCodes.Status304NotModified; context.Response.ContentLength = 0; context.Response.ContentType = null; return; } GlobalPerf.Singleton.IncrementCounter("etag_miss"); if (info.HasParams) { logger?.LogInformation("Processing image {VirtualPath} with params {CommandString}", info.FinalVirtualPath, info.CommandString); GlobalPerf.Singleton.IncrementCounter("nocache_processed"); var imageData = await info.ProcessUncached(); var imageBytes = imageData.ResultBytes; var contentType = imageData.ContentType; // write to stream context.Response.ContentType = contentType; context.Response.ContentLength = imageBytes.Count; SetCachingHeaders(context, betterCacheKey); if (imageBytes.Array == null) { throw new InvalidOperationException("Image job returned zero bytes."); } await context.Response.Body.WriteAsync(imageBytes.Array, imageBytes.Offset, imageBytes.Count); } else { logger?.LogInformation("Proxying image {VirtualPath} with params {CommandString}", info.FinalVirtualPath, info.CommandString); GlobalPerf.Singleton.IncrementCounter("nocache_proxied"); await using var sourceStream = (await info.GetPrimaryBlob()).OpenRead(); SetCachingHeaders(context, betterCacheKey); await MagicBytes.ProxyToStream(sourceStream, context.Response); } }
public async Task Invoke(HttpContext context) { var path = context.Request.Path; // Delegate to the diagnostics page if it is requested if (diagnosticsPage.MatchesPath(path.Value)) { await diagnosticsPage.Invoke(context); return; } // We only handle requests with an image extension, period. if (!PathHelpers.IsImagePath(path)) { await next.Invoke(context); return; } var imageJobInfo = new ImageJobInfo(context, options, blobProvider); if (!imageJobInfo.Authorized) { await NotAuthorized(context); return; } // If the file is definitely missing hand to the next middleware // Remote providers will fail late rather than make 2 requests if (!imageJobInfo.PrimaryBlobMayExist()) { await next.Invoke(context); return; } var memoryCacheEnabled = memoryCache != null && options.AllowMemoryCaching && imageJobInfo.NeedsCaching(); var diskCacheEnabled = diskCache != null && options.AllowDiskCaching && imageJobInfo.NeedsCaching(); var distributedCacheEnabled = distributedCache != null && options.AllowDistributedCaching && imageJobInfo.NeedsCaching(); var sqliteCacheEnabled = sqliteCache != null && options.AllowSqliteCaching && imageJobInfo.NeedsCaching(); string cacheKey = null; if (memoryCacheEnabled || diskCacheEnabled || distributedCacheEnabled | sqliteCacheEnabled) { cacheKey = await imageJobInfo.GetFastCacheKey(); if (context.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var etag) && cacheKey == etag) { context.Response.StatusCode = StatusCodes.Status304NotModified; context.Response.ContentLength = 0; context.Response.ContentType = null; return; } } try { if (sqliteCacheEnabled) { await ProcessWithSqliteCache(context, cacheKey, imageJobInfo); } else if (diskCacheEnabled) { await ProcessWithDiskCache(context, cacheKey, imageJobInfo); } else if (memoryCacheEnabled) { await ProcessWithMemoryCache(context, cacheKey, imageJobInfo); // ReSharper disable once ConditionIsAlwaysTrueOrFalse } else if (distributedCacheEnabled) { await ProcessWithDistributedCache(context, cacheKey, imageJobInfo); } else { await ProcessWithNoCache(context, imageJobInfo); } } catch (BlobMissingException e) { await NotFound(context, e); } }
private async Task ProcessWithSqliteCache(HttpContext context, string cacheKey, ImageJobInfo info) { var cacheResult = await sqliteCache.GetOrCreate(cacheKey, async() => { if (info.HasParams) { logger?.LogInformation($"Sqlite Cache Miss: Processing image {info.FinalVirtualPath}?{info.CommandString}"); var imageData = await info.ProcessUncached(); var imageBytes = imageData.ResultBytes.Count != imageData.ResultBytes.Array?.Length ? imageData.ResultBytes.ToArray() : imageData.ResultBytes.Array; var contentType = imageData.ContentType; return(new SqliteCacheEntry() { ContentType = contentType, Data = imageBytes }); } else { logger?.LogInformation($"Sqlite Cache Miss: Proxying image {info.FinalVirtualPath}?{info.CommandString}"); var contentType = PathHelpers.ContentTypeForImageExtension(info.EstimatedFileExtension); return(new SqliteCacheEntry() { ContentType = contentType, Data = await info.GetPrimaryBlobBytesAsync() }); } }); // write to stream context.Response.ContentType = cacheResult.ContentType; context.Response.ContentLength = cacheResult.Data.Length; SetCachingHeaders(context, cacheKey); await context.Response.Body.WriteAsync(cacheResult.Data, 0, cacheResult.Data.Length); }
private async Task ProcessWithDistributedCache(HttpContext context, string cacheKey, ImageJobInfo info) { var imageBytes = await distributedCache.GetAsync(cacheKey); var contentType = await distributedCache.GetStringAsync(cacheKey + ".contentType"); if (imageBytes != null && contentType != null) { logger?.LogInformation("Serving {0}?{1} from distributed cache", info.FinalVirtualPath, info.CommandString); } else { if (info.HasParams) { logger?.LogInformation($"Distributed Cache Miss: Processing image {info.FinalVirtualPath}?{info.CommandString}"); var imageData = await info.ProcessUncached(); imageBytes = imageData.ResultBytes.Count != imageData.ResultBytes.Array?.Length ? imageData.ResultBytes.ToArray() : imageData.ResultBytes.Array; contentType = imageData.ContentType; } else { logger?.LogInformation($"Distributed Cache Miss: Proxying image {info.FinalVirtualPath}?{info.CommandString}"); contentType = PathHelpers.ContentTypeForImageExtension(info.EstimatedFileExtension); imageBytes = await info.GetPrimaryBlobBytesAsync(); } // Set cache options. var cacheEntryOptions = new DistributedCacheEntryOptions() .SetSlidingExpiration(options.DistributedCacheSlidingExpiration); await distributedCache.SetAsync(cacheKey, imageBytes, cacheEntryOptions); await distributedCache.SetStringAsync(cacheKey + ".contentType", contentType, cacheEntryOptions); } // write to stream context.Response.ContentType = contentType; context.Response.ContentLength = imageBytes.Length; SetCachingHeaders(context, cacheKey); await context.Response.Body.WriteAsync(imageBytes, 0, imageBytes.Length); }
private async Task ProcessWithMemoryCache(HttpContext context, string cacheKey, ImageJobInfo info) { var isCached = memoryCache.TryGetValue(cacheKey, out ArraySegment <byte> imageBytes); var isContentTypeCached = memoryCache.TryGetValue(cacheKey + ".contentType", out string contentType); if (isCached && isContentTypeCached) { logger?.LogInformation("Serving {0}?{1} from memory cache", info.FinalVirtualPath, info.CommandString); } else { if (info.HasParams) { logger?.LogInformation($"Memory Cache Miss: Processing image {info.FinalVirtualPath}?{info.CommandString}"); var imageData = await info.ProcessUncached(); imageBytes = imageData.ResultBytes; contentType = imageData.ContentType; } else { logger?.LogInformation($"Memory Cache Miss: Proxying image {info.FinalVirtualPath}?{info.CommandString}"); contentType = PathHelpers.ContentTypeForImageExtension(info.EstimatedFileExtension); imageBytes = new ArraySegment <byte>(await info.GetPrimaryBlobBytesAsync()); } // Set cache options. var cacheEntryOptions = new MemoryCacheEntryOptions() .SetSize(imageBytes.Count) .SetSlidingExpiration(options.MemoryCacheSlidingExpiration); var cacheEntryMetaOptions = new MemoryCacheEntryOptions() .SetSize(contentType.Length * 2) .SetSlidingExpiration(options.MemoryCacheSlidingExpiration); memoryCache.Set(cacheKey, imageBytes, cacheEntryOptions); memoryCache.Set(cacheKey + ".contentType", contentType, cacheEntryMetaOptions); } // write to stream context.Response.ContentType = contentType; context.Response.ContentLength = imageBytes.Count; SetCachingHeaders(context, cacheKey); await context.Response.Body.WriteAsync(imageBytes.Array, imageBytes.Offset, imageBytes.Count); }
private async Task ProcessWithDiskCache(HttpContext context, string cacheKey, ImageJobInfo info) { var cacheResult = await diskCache.GetOrCreate(cacheKey, info.EstimatedFileExtension, async (stream) => { if (info.HasParams) { logger?.LogInformation($"DiskCache Miss: Processing image {info.FinalVirtualPath}?{info}"); var result = await info.ProcessUncached(); await stream.WriteAsync(result.ResultBytes.Array, result.ResultBytes.Offset, result.ResultBytes.Count, CancellationToken.None); await stream.FlushAsync(); } else { logger?.LogInformation($"DiskCache Miss: Proxying image {info.FinalVirtualPath}"); await info.CopyPrimaryBlobToAsync(stream); } }); // Note that using estimated file extension instead of parsing magic bytes will lead to incorrect content-type // values when the source file has a mismatched extension. if (cacheResult.Data != null) { if (cacheResult.Data.Length < 1) { throw new InvalidOperationException("DiskCache returned cache entry with zero bytes"); } context.Response.ContentType = PathHelpers.ContentTypeForImageExtension(info.EstimatedFileExtension); context.Response.ContentLength = cacheResult.Data.Length; //ReadOnlyMemoryStream, so it supports seeking SetCachingHeaders(context, cacheKey); await cacheResult.Data.CopyToAsync(context.Response.Body); } else { logger?.LogInformation("Serving {0}?{1} from disk cache {2}", info.FinalVirtualPath, info.CommandString, cacheResult.RelativePath); await ServeFileFromDisk(context, cacheResult.PhysicalPath, cacheKey, PathHelpers.ContentTypeForImageExtension(info.EstimatedFileExtension)); } }
private async Task ProcessWithDiskCache(HttpContext context, string cacheKey, ImageJobInfo info) { var cacheResult = await diskCache.GetOrCreate(cacheKey, info.EstimatedFileExtension, async (stream) => { if (info.HasParams) { logger?.LogInformation("DiskCache Miss: Processing image {VirtualPath}{QueryString}", info.FinalVirtualPath, info); var result = await info.ProcessUncached(); if (result.ResultBytes.Array == null) { throw new InvalidOperationException("Image job returned zero bytes."); } await stream.WriteAsync(result.ResultBytes.Array, result.ResultBytes.Offset, result.ResultBytes.Count, CancellationToken.None); await stream.FlushAsync(); } else { logger?.LogInformation("DiskCache Miss: Proxying image {VirtualPath}", info.FinalVirtualPath); await info.CopyPrimaryBlobToAsync(stream); } }); if (cacheResult.Result == CacheQueryResult.Miss) { GlobalPerf.Singleton.IncrementCounter("diskcache_miss"); } else if (cacheResult.Result == CacheQueryResult.Hit) { GlobalPerf.Singleton.IncrementCounter("diskcache_hit"); } else if (cacheResult.Result == CacheQueryResult.Failed) { GlobalPerf.Singleton.IncrementCounter("diskcache_timeout"); } // Note that using estimated file extension instead of parsing magic bytes will lead to incorrect content-type // values when the source file has a mismatched extension. if (cacheResult.Data != null) { if (cacheResult.Data.Length < 1) { throw new InvalidOperationException("DiskCache returned cache entry with zero bytes"); } SetCachingHeaders(context, cacheKey); await MagicBytes.ProxyToStream(cacheResult.Data, context.Response); } else { logger?.LogInformation("Serving {0}?{1} from disk cache {2}", info.FinalVirtualPath, info.CommandString, cacheResult.RelativePath); await ServeFileFromDisk(context, cacheResult.PhysicalPath, cacheKey); } }
private async Task ProcessWithStreamCache(HttpContext context, string cacheKey, ImageJobInfo info) { var keyBytes = Encoding.UTF8.GetBytes(cacheKey); var typeName = streamCache.GetType().Name; var cacheResult = await streamCache.GetOrCreateBytes(keyBytes, async (cancellationToken) => { if (info.HasParams) { logger?.LogDebug("{CacheName} miss: Processing image {VirtualPath}?{Querystring}", typeName, info.FinalVirtualPath, info.ToString()); var result = await info.ProcessUncached(); if (result.ResultBytes.Array == null) { throw new InvalidOperationException("Image job returned zero bytes."); } return(new Tuple <string, ArraySegment <byte> >(result.ContentType, result.ResultBytes)); } logger?.LogDebug("{CacheName} miss: Proxying image {VirtualPath}", typeName, info.FinalVirtualPath); var bytes = await info.GetPrimaryBlobBytesAsync(); return(new Tuple <string, ArraySegment <byte> >(null, bytes)); }, CancellationToken.None, false); if (cacheResult.Status != null) { GlobalPerf.Singleton.IncrementCounter($"{typeName}_{cacheResult.Status}"); } if (cacheResult.Data != null) { await using (cacheResult.Data) { if (cacheResult.Data.Length < 1) { throw new InvalidOperationException($"{typeName} returned cache entry with zero bytes"); } SetCachingHeaders(context, cacheKey); await MagicBytes.ProxyToStream(cacheResult.Data, context.Response); } logger?.LogDebug("Serving from {CacheName} {VirtualPath}?{CommandString}", typeName, info.FinalVirtualPath, info.CommandString); } else { // TODO explore this failure path better throw new NullReferenceException("Caching failed: " + cacheResult.Status); } }
// ReSharper disable once UnusedMember.Global public async Task Invoke(HttpContext context) { // For instrumentation globalInfoProvider.CopyHttpContextInfo(context); var path = context.Request.Path; // Delegate to the diagnostics page if it is requested if (DiagnosticsPage.MatchesPath(path.Value)) { await diagnosticsPage.Invoke(context); return; } // Delegate to licenses page if requested if (licensePage.MatchesPath(path.Value)) { await licensePage.Invoke(context); return; } // Respond to /imageflow.ready if ("/imageflow.ready".Equals(path.Value, StringComparison.Ordinal)) { options.Licensing.FireHeartbeat(); using (new JobContext()) { await StringResponseNoCache(context, 200, "Imageflow.Server is ready to accept requests."); } return; } // Respond to /imageflow.health if ("/imageflow.health".Equals(path.Value, StringComparison.Ordinal)) { options.Licensing.FireHeartbeat(); await StringResponseNoCache(context, 200, "Imageflow.Server is healthy."); return; } // We only handle requests with an image extension or if we configured a path prefix for which to handle // extensionless requests if (!ImageJobInfo.ShouldHandleRequest(context, options)) { await next.Invoke(context); return; } options.Licensing.FireHeartbeat(); var imageJobInfo = new ImageJobInfo(context, options, blobProvider); if (!imageJobInfo.Authorized) { await NotAuthorized(context, imageJobInfo.AuthorizedMessage); return; } if (imageJobInfo.LicenseError) { if (options.EnforcementMethod == EnforceLicenseWith.Http422Error) { await StringResponseNoCache(context, 422, options.Licensing.InvalidLicenseMessage); return; } if (options.EnforcementMethod == EnforceLicenseWith.Http402Error) { await StringResponseNoCache(context, 402, options.Licensing.InvalidLicenseMessage); return; } } // If the file is definitely missing hand to the next middleware // Remote providers will fail late rather than make 2 requests if (!imageJobInfo.PrimaryBlobMayExist()) { await next.Invoke(context); return; } string cacheKey = null; var cachingPath = imageJobInfo.NeedsCaching() ? options.ActiveCacheBackend : CacheBackend.NoCache; if (cachingPath != CacheBackend.NoCache) { cacheKey = await imageJobInfo.GetFastCacheKey(); if (context.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var etag) && cacheKey == etag) { GlobalPerf.Singleton.IncrementCounter("etag_hit"); context.Response.StatusCode = StatusCodes.Status304NotModified; context.Response.ContentLength = 0; context.Response.ContentType = null; return; } GlobalPerf.Singleton.IncrementCounter("etag_miss"); } try { switch (cachingPath) { case CacheBackend.ClassicDiskCache: await ProcessWithDiskCache(context, cacheKey, imageJobInfo); break; case CacheBackend.NoCache: await ProcessWithNoCache(context, imageJobInfo); break; case CacheBackend.StreamCache: await ProcessWithStreamCache(context, cacheKey, imageJobInfo); break; default: throw new ArgumentOutOfRangeException(); } GlobalPerf.Singleton.IncrementCounter("middleware_ok"); } catch (BlobMissingException e) { await NotFound(context, e); } catch (Exception e) { var errorName = e.GetType().Name; var errorCounter = "middleware_" + errorName; GlobalPerf.Singleton.IncrementCounter(errorCounter); GlobalPerf.Singleton.IncrementCounter("middleware_errors"); throw; } finally { // Increment counter for type of file served var imageExtension = PathHelpers.GetImageExtensionFromContentType(context.Response.ContentType); if (imageExtension != null) { GlobalPerf.Singleton.IncrementCounter("module_response_ext_" + imageExtension); } } }