Пример #1
0
        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);
                }
            }
        }
Пример #2
0
        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);
            }
        }
Пример #3
0
        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);
            }
        }
Пример #4
0
        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);
        }
Пример #5
0
        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);
        }
Пример #6
0
        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);
        }
Пример #7
0
        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));
            }
        }
Пример #8
0
        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);
            }
        }
Пример #9
0
        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);
            }
        }
Пример #10
0
        // 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);
                }
            }
        }