protected override async Task ProcessImageAsync(MediaHandlerContext context, CachedImage cachedImage, Stream inputStream) { var processQuery = new ProcessImageQuery(context.ImageQuery) { Source = inputStream, Format = context.ImageQuery.Format ?? cachedImage.Extension, FileName = cachedImage.FileName, DisposeSource = false }; using (var result = await _imageProcessor.ProcessImageAsync(processQuery, false)) { Logger.Debug($"Processed image '{cachedImage.FileName}' in {result.ProcessTimeMs} ms."); var ext = result.Image.Format.DefaultExtension; if (!cachedImage.Extension.EqualsNoCase(ext)) { // jpg <> jpeg cachedImage.Path = Path.ChangeExtension(cachedImage.Path, ext); cachedImage.Extension = ext; } context.ResultImage = result.Image; } }
public async Task Invoke( HttpContext context, IMediaService mediaService, IFolderService folderService, IPermissionService permissionService, IWorkContext workContext, MediaSettings mediaSettings, MediaHelper mediaHelper, Lazy <IEnumerable <IMediaHandler> > mediaHandlers, ILogger <MediaMiddleware> logger) { var mediaFileId = context.GetRouteValueAs <int>("id"); var path = context.GetRouteValueAs <string>("path"); if (context.Request.Method != HttpMethods.Get && context.Request.Method != HttpMethods.Head) { await NotFound(null); return; } var method = context.Request.Method; MediaFileInfo mediaFile = null; MediaPathData pathData = null; if (mediaFileId == 0) { // This is most likely a request for a default placeholder image pathData = new MediaPathData(path); } else if (!mediaHelper.TokenizePath(path, false, out pathData)) { // Missing or malformed Uri: get file metadata from DB by id, but only when current user has media manage rights if (!(await permissionService.AuthorizeAsync(Permissions.Media.Update))) { await NotFound(null); return; } mediaFile = await mediaService.GetFileByIdAsync(mediaFileId, MediaLoadFlags.AsNoTracking); if (mediaFile == null || mediaFile.FolderId == null || mediaFile.Deleted) { await NotFound(mediaFile?.MimeType); return; } pathData = new MediaPathData(folderService.GetNodeById(mediaFile.FolderId.Value), mediaFile.Name) { Extension = mediaFile.Extension, MimeType = mediaFile.MimeType }; } var q = await CreateImageQuery(context, pathData.MimeType, pathData.Extension); // Security: check allowed thumnail sizes and return 404 if disallowed. var thumbMaxWidth = q.MaxWidth; var thumbMaxHeight = q.MaxHeight; var thumbSizeAllowed = IsThumbnailSizeAllowed(thumbMaxWidth) && (thumbMaxHeight == thumbMaxWidth || IsThumbnailSizeAllowed(thumbMaxHeight)); if (!thumbSizeAllowed) { await NotFound(pathData.MimeType); return; } // Create the handler context var handlerContext = new MediaHandlerContext { HttpContext = context, CurrentCustomer = workContext.CurrentCustomer, PermissionService = permissionService, MediaFileId = mediaFileId, RawPath = path, MediaService = mediaService, PathData = pathData, ImageQuery = q }; handlerContext.SetSourceFile(mediaFile); var handlers = mediaHandlers.Value.OrderBy(x => x.Order).ToArray(); // Run every registered media handler to obtain a thumbnail for the requested media file IMediaHandler currentHandler; for (var i = 0; i < handlers.Length; i++) { currentHandler = handlers[i]; // Execute handler await currentHandler.ExecuteAsync(handlerContext); if (handlerContext.Exception != null) { var isThumbExtractFail = handlerContext.Exception is ExtractThumbnailException; var statusCode = isThumbExtractFail ? StatusCodes.Status204NoContent : StatusCodes.Status500InternalServerError; var statusMessage = isThumbExtractFail ? handlerContext.Exception.InnerException?.Message.EmptyNull() : handlerContext.Exception.Message; await SendStatus(statusCode, statusMessage); return; } if (handlerContext.Executed || handlerContext.ResultFile != null) { // Get out if the handler produced a result file or has been executed in any way break; } } try { var responseFile = handlerContext.ResultFile ?? await handlerContext.GetSourceFileAsync(); if (responseFile == null || !responseFile.Exists) { await NotFound(pathData.MimeType); return; } if (string.Equals(responseFile.Extension, "." + pathData.Extension, StringComparison.CurrentCultureIgnoreCase)) { pathData.MimeType = MimeTypes.MapNameToMimeType(responseFile.Extension); } // Create FileStreamResult object var fileResult = CreateFileResult(responseFile, pathData); // Cache control ApplyResponseCaching(context, mediaSettings); // INFO: Although we are outside of the MVC pipeline we gonna use ActionContext anyway, because "FileStreamResult" // does everything we need (ByteRange, ETag etc.), so wo we gonna use it instead of reinventing the wheel. // A look at the MVC source code reveals that HttpContext is the only property that gets accessed, therefore we can omit // all the other stuff like ActionDescriptor or ModelState (which we cannot access or create from a middleware anyway). await fileResult.ExecuteResultAsync(new ActionContext { HttpContext = context, RouteData = context.GetRouteData() }); } finally { var imageProcessor = context.RequestServices.GetRequiredService <IImageProcessor>(); logger.Debug("ImageProcessor TOTAL: {0} ms.", imageProcessor.TotalProcessingTimeMs); } #region Functions bool IsThumbnailSizeAllowed(int?size) { return(size.GetValueOrDefault() == 0 || mediaSettings.IsAllowedThumbnailSize(size.Value) || permissionService.Authorize(Permissions.Media.Update, workContext.CurrentCustomer)); } async Task NotFound(string mime) { context.Response.ContentType = mime.NullEmpty() ?? "text/html"; context.Response.StatusCode = 404; await context.Response.WriteAsync("404: Not Found"); } async Task SendStatus(int code, string message) { context.Response.StatusCode = code; await context.Response.WriteAsync(message); } #endregion }
public async Task ExecuteAsync(MediaHandlerContext context) { if (!IsProcessable(context)) { return; } var query = context.ImageQuery; var pathData = context.PathData; var cachedImage = await ImageCache.GetAsync(context.MediaFileId, pathData, query); if (!pathData.Extension.EqualsNoCase(cachedImage.Extension)) { // The query requests another format. // Adjust extension and mime type fo proper ETag creation. pathData.Extension = cachedImage.Extension; pathData.MimeType = cachedImage.MimeType; } var exists = cachedImage.Exists; if (exists && cachedImage.FileSize == 0) { // Empty file means: thumb extraction failed before and will most likely fail again. // Don't bother proceeding. context.Exception = ExceptionFactory.ExtractThumbnail(cachedImage.FileName); context.Executed = true; return; } if (!exists) { // Lock concurrent requests to same resource using (await AsyncLock.KeyedAsync("ImageHandlerBase.Execute." + cachedImage.Path)) { await ImageCache.RefreshInfoAsync(cachedImage); // File could have been processed by another request in the meantime, check again. if (!cachedImage.Exists) { // Call inner function var sourceFile = await context.GetSourceFileAsync(); if (sourceFile == null || sourceFile.Length == 0) { context.Executed = true; return; } var inputStream = await sourceFile.OpenReadAsync(); if (inputStream == null) { context.Exception = ExceptionFactory.ExtractThumbnail(sourceFile.SubPath, T("Admin.Media.Exception.NullInputStream")); context.Executed = true; return; } try { await ProcessImageAsync(context, cachedImage, inputStream); } catch (Exception ex) { Logger.Error(ex); if (ex is ExtractThumbnailException) { // Thumbnail extraction failed and we must assume that it always will fail. // Therefore we create an empty file to prevent repetitive processing. using (var memStream = new MemoryStream()) { await ImageCache.PutAsync(cachedImage, memStream); } } context.Exception = ex; context.Executed = true; return; } finally { if (inputStream != null) { inputStream.Dispose(); } } if (context.ResultImage != null) { await ImageCache.PutAsync(cachedImage, context.ResultImage); context.ResultFile = cachedImage.File; } context.Executed = true; return; } } } // Cached image existed already context.ResultFile = cachedImage.File; context.Executed = true; }
protected abstract Task ProcessImageAsync(MediaHandlerContext context, CachedImage cachedImage, Stream inputStream);
protected abstract bool IsProcessable(MediaHandlerContext context);
protected override bool IsProcessable(MediaHandlerContext context) { return(context.ImageQuery.NeedsProcessing(true) && _imageProcessor.Factory.IsSupportedImage(context.PathData.Extension)); }