Exemple #1
0
 public ImageWorkerResult(ImageCacheMetadata cacheImageMetadata, IImageCacheResolver resolver)
 {
     this.IsNewOrUpdated      = false;
     this.SourceImageMetadata = default;
     this.CacheImageMetadata  = cacheImageMetadata;
     this.Resolver            = resolver;
 }
        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).ConfigureAwait(false))
                {
                    // Check to see if the cache contains this image
                    sourceImageMetadata = await sourceImageResolver.GetMetaDataAsync().ConfigureAwait(false);

                    IImageCacheResolver cachedImageResolver = await this.cache.GetAsync(key).ConfigureAwait(false);

                    if (cachedImageResolver != null)
                    {
                        ImageCacheMetadata cachedImageMetadata = await cachedImageResolver.GetMetaDataAsync().ConfigureAwait(false);

                        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().ConfigureAwait(false))
                                {
                                    await this.SendResponseAsync(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.
                            ImageCacheMetadata 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 ImageCacheMetadata(
                                        sourceImageMetadata.LastWriteTimeUtc,
                                        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.SendResponseAsync(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();
                }
            }
        }
Exemple #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.
            ImageWorkerResult readResult = default;

            try
            {
                readResult = await this.IsNewOrUpdatedAsync(sourceImageResolver, imageContext, key);
            }
            finally
            {
                ReadWorkers.TryRemove(key, out Task <ImageWorkerResult> _);
            }

            if (!readResult.IsNewOrUpdated)
            {
                await this.SendResponseAsync(imageContext, key, readResult.CacheImageMetadata, readResult.Resolver);

                return;
            }

            // Not cached, or is updated? Let's get it from the image resolver.
            var sourceImageMetadata = readResult.SourceImageMetadata;

            // Enter an asynchronous write worker which prevents multiple writes and delays any reads for the same request.
            // This reduces the overheads of unnecessary processing.
            try
            {
                ImageWorkerResult writeResult = await WriteWorkers.GetOrAddAsync(
                    key,
                    async (key) =>
                {
                    RecyclableMemoryStream outStream = null;
                    try
                    {
                        // Prevent a second request from starting a read during write execution.
                        if (ReadWorkers.TryGetValue(key, out Task <ImageWorkerResult> readWork))
                        {
                            await readWork;
                        }

                        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 any resolver from the cache so we always resolve next request
                        // for the same key.
                        CacheResolverLru.TryRemove(key);

                        // Place the resolver in the lru cache.
                        (IImageCacheResolver ImageCacheResolver, ImageCacheMetadata ImageCacheMetadata)cachedImage = await
                                                                                                                     CacheResolverLru.GetOrAddAsync(
                            key,
                            async k =>
                        {
                            IImageCacheResolver resolver = await this.cache.GetAsync(k);
                            ImageCacheMetadata metadata  = default;
                            if (resolver != null)
                            {
                                metadata = await resolver.GetMetaDataAsync();
                            }

                            return(resolver, metadata);
                        });

                        return(new ImageWorkerResult(cachedImage.ImageCacheMetadata, cachedImage.ImageCacheResolver));
                    }
                    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);
                    }
                });

                await this.SendResponseAsync(imageContext, key, writeResult.CacheImageMetadata, writeResult.Resolver);
            }
            finally
            {
                // As soon as we have sent a response from a writer the result is available from a reader so we remove this task.
                // Any existing awaiters will continue to await.
                WriteWorkers.TryRemove(key, out Task <ImageWorkerResult> _);
            }
        }