/// <inheritdoc/> public async Task <ImageCacheMetadata> GetMetaDataAsync() { using Stream stream = this.metaFileInfo.CreateReadStream(); this.metadata = await ImageCacheMetadata.ReadAsync(stream); return(this.metadata); }
/// <inheritdoc/> public async Task <IImageCacheResolver> GetAsync(string key) { string path = this.ToFilePath(key); IFileInfo metaFileInfo = this.fileProvider.GetFileInfo(this.ToMetaDataFilePath(path)); if (!metaFileInfo.Exists) { return(null); } ImageCacheMetadata metadata = default; using (Stream stream = metaFileInfo.CreateReadStream()) { metadata = await ImageCacheMetadata.ReadAsync(stream).ConfigureAwait(false); } IFileInfo fileInfo = this.fileProvider.GetFileInfo(this.ToImageFilePath(path, metadata)); // Check to see if the file exists. if (!fileInfo.Exists) { return(null); } return(new PhysicalFileSystemCacheResolver(fileInfo, metadata)); }
public ImageWorkerResult(ImageCacheMetadata cacheImageMetadata, IImageCacheResolver resolver) { this.IsNewOrUpdated = false; this.SourceImageMetadata = default; this.CacheImageMetadata = cacheImageMetadata; this.Resolver = resolver; }
/// <inheritdoc/> public async Task <ImageCacheMetadata> GetMetaDataAsync() { if (this.metadata == default) { using FileStream stream = OpenFileStream(this.metaFileInfo.FullName); this.metadata = await ImageCacheMetadata.ReadAsync(stream); } return(this.metadata); }
/// <inheritdoc/> public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata) { BlobClient blob = this.container.GetBlobClient(key); var headers = new BlobHttpHeaders { ContentType = metadata.ContentType, }; return(blob.UploadAsync(stream, httpHeaders: headers, metadata: metadata.ToDictionary())); }
/// <inheritdoc/> public Task <ImageCacheMetadata> GetMetaDataAsync() { Dictionary <string, string> dict = new(); foreach (string key in this.metadata.Keys) { // Trim automatically added x-amz-meta- dict.Add(key.Substring(11).ToUpperInvariant(), this.metadata[key]); } return(Task.FromResult(ImageCacheMetadata.FromDictionary(dict))); }
/// <inheritdoc/> public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata) { var request = new PutObjectRequest() { BucketName = this.bucket, Key = key, ContentType = metadata.ContentType, InputStream = stream, AutoCloseStream = false }; foreach (KeyValuePair <string, string> d in metadata.ToDictionary()) { request.Metadata.Add(d.Key, d.Value); } return(this.amazonS3Client.PutObjectAsync(request)); }
/// <inheritdoc/> public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata) { string path = Path.Combine(this.environment.WebRootPath, this.ToFilePath(key)); string imagePath = this.ToImageFilePath(path, metadata); string metaPath = this.ToMetaDataFilePath(path); string directory = Path.GetDirectoryName(path); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } using (FileStream fileStream = File.Create(imagePath)) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } using (FileStream fileStream = File.Create(metaPath)) { await metadata.WriteAsync(fileStream).ConfigureAwait(false); } }
public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata) => Task.CompletedTask;
private async Task SendResponseAsync(ImageContext imageContext, string key, Stream stream, ImageCacheMetadata metadata) { imageContext.ComprehendRequestHeaders(metadata.CacheLastWriteTimeUtc, stream.Length); switch (imageContext.GetPreconditionState()) { case ImageContext.PreconditionState.Unspecified: case ImageContext.PreconditionState.ShouldProcess: if (imageContext.IsHeadRequest()) { await imageContext.SendStatusAsync(ResponseConstants.Status200Ok, metadata).ConfigureAwait(false); } this.logger.LogImageServed(imageContext.GetDisplayUrl(), key); await imageContext.SendAsync(stream, metadata).ConfigureAwait(false); break; case ImageContext.PreconditionState.NotModified: this.logger.LogImageNotModified(imageContext.GetDisplayUrl()); await imageContext.SendStatusAsync(ResponseConstants.Status304NotModified, metadata).ConfigureAwait(false); break; case ImageContext.PreconditionState.PreconditionFailed: this.logger.LogImagePreconditionFailed(imageContext.GetDisplayUrl()); await imageContext.SendStatusAsync(ResponseConstants.Status412PreconditionFailed, metadata).ConfigureAwait(false); break; default: var exception = new NotImplementedException(imageContext.GetPreconditionState().ToString()); Debug.Fail(exception.ToString()); throw exception; } }
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(); } } }
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> _); } }
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; }
public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata) { return(Task.CompletedTask); }