/// <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);
            }
        }
Example #2
0
        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();
                }
            }
        }
Example #3
0
        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);
            }
        }
Example #5
0
        /// <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);
            }
        }