/// <summary> /// Post processes the image. /// </summary> /// <param name="context">The current context.</param> /// <param name="stream">The source image stream.</param> /// <param name="extension">The image extension.</param> /// <returns> /// The <see cref="MemoryStream" />. /// </returns> public static async Task <MemoryStream> PostProcessImageAsync(HttpContext context, MemoryStream stream, string extension) { var postProcessorBootstrapper = PostProcessorBootstrapper.Instance; if (postProcessorBootstrapper.IsInstalled && stream.Length is var length && length > 0) { string sourceFile = null, destinationFile = null; try { // Get temporary file names var tempPath = Path.GetTempPath(); var tempFile = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); sourceFile = Path.Combine(tempPath, Path.ChangeExtension(tempFile, extension)); destinationFile = Path.Combine(tempPath, Path.ChangeExtension(tempFile + "-out", extension)); // Get processes to start var processStartInfos = GetProcessStartInfos(extension, length, sourceFile, destinationFile).ToList(); if (processStartInfos.Count > 0) { // Save the input stream to our source temp file for post processing var sourceFileInfo = new FileInfo(sourceFile); using (var fileStream = sourceFileInfo.Create()) { // Try to keep the file in memory and ensure it's not indexed sourceFileInfo.Attributes |= FileAttributes.Temporary | FileAttributes.NotContentIndexed; await stream.CopyToAsync(fileStream).ConfigureAwait(false); } // Create cancellation token with timeout using (var cancellationTokenSource = new CancellationTokenSource(postProcessorBootstrapper.Timeout)) { var remainingProcesses = processStartInfos.Count; foreach (var processStartInfo in processStartInfos) { // Set default properties processStartInfo.FileName = Path.Combine(postProcessorBootstrapper.WorkingPath, processStartInfo.FileName); processStartInfo.CreateNoWindow = true; processStartInfo.WindowStyle = ProcessWindowStyle.Hidden; // Run process using (var processResults = await ProcessEx.RunAsync(processStartInfo, cancellationTokenSource.Token).ConfigureAwait(false)) { if (processResults.ExitCode == 1) { ImageProcessorBootstrapper.Instance.Logger.Log(typeof(PostProcessor), $"Unable to post process image for request {context.Request.Unvalidated.Url}, {processStartInfo.FileName} {processStartInfo.Arguments} exited with error code 1. Original image returned."); break; } } remainingProcesses--; var destinationFileInfo = new FileInfo(destinationFile); if (destinationFileInfo.Exists) { // Delete the source file sourceFileInfo.IsReadOnly = false; sourceFileInfo.Delete(); if (remainingProcesses > 0) { // Use destination file as new source (for the next process) destinationFileInfo.MoveTo(sourceFile); } // Swap source for destination sourceFileInfo = destinationFileInfo; // Try to keep the file in memory and ensure it's not indexed sourceFileInfo.Attributes |= FileAttributes.Temporary | FileAttributes.NotContentIndexed; } } } // Refresh source file (because it's changed by external processes) sourceFileInfo.Refresh(); if (sourceFileInfo.Exists && sourceFileInfo.Length < length) { // Save result back to stream using (var fileStream = sourceFileInfo.OpenRead()) { stream.SetLength(0); await fileStream.CopyToAsync(stream).ConfigureAwait(false); } } } } catch (OperationCanceledException) { ImageProcessorBootstrapper.Instance.Logger.Log(typeof(PostProcessor), $"Unable to post process image for request {context.Request.Unvalidated.Url} within {postProcessorBootstrapper.Timeout}ms. Original image returned."); } catch (Exception ex) { // Some security policies don't allow execution of programs in this way ImageProcessorBootstrapper.Instance.Logger.Log(typeof(PostProcessor), ex.Message); } finally { // Set position back to begin stream.Position = 0; // Always cleanup files if (sourceFile != null) { try { var sourceFileInfo = new FileInfo(sourceFile); if (sourceFileInfo.Exists) { sourceFileInfo.IsReadOnly = false; sourceFileInfo.Delete(); } } catch { // Normally a no no, but logging would be excessive + temp files get cleaned up eventually } } if (destinationFile != null) { try { var destinationFileInfo = new FileInfo(destinationFile); if (destinationFileInfo.Exists) { destinationFileInfo.IsReadOnly = false; destinationFileInfo.Delete(); } } catch { // Normally a no no, but logging would be excessive + temp files get cleaned up eventually } } } } // ALways return stream (even if it's not optimized) return(stream); }
/// <summary> /// Post processes the image. /// </summary> /// <param name="context">The current context.</param> /// <param name="stream">The source image stream.</param> /// <param name="extension">The image extension.</param> /// <returns> /// The <see cref="MemoryStream" />. /// </returns> public static async Task <MemoryStream> PostProcessImageAsync(HttpContext context, MemoryStream stream, string extension) { if (!PostProcessorBootstrapper.Instance.IsInstalled) { return(stream); } string sourceFile = null, destinationFile = null; var timeout = PostProcessorBootstrapper.Instance.Timout; try { // Save source file length var length = stream.Length; // Create a temporary source file with the correct extension var tempSourceFile = Path.GetTempFileName(); sourceFile = Path.ChangeExtension(tempSourceFile, extension); File.Move(tempSourceFile, sourceFile); // Give our destination file a unique name destinationFile = sourceFile.Replace(extension, "-out" + extension); // Get processes to start var processStartInfos = GetProcessStartInfos(extension, length, sourceFile, destinationFile).ToList(); if (processStartInfos.Count > 0) { // Save the input stream to our source temp file for post processing using (var fileStream = File.Create(sourceFile)) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } // Create cancellation token with timeout using (var cancellationTokenSource = new CancellationTokenSource(timeout)) { foreach (var processStartInfo in processStartInfos) { // Use destination file as new source (if previous process created one). if (File.Exists(destinationFile)) { File.Copy(destinationFile, sourceFile, true); } // Set default properties processStartInfo.FileName = Path.Combine(PostProcessorBootstrapper.Instance.WorkingPath, processStartInfo.FileName); processStartInfo.CreateNoWindow = true; processStartInfo.WindowStyle = ProcessWindowStyle.Hidden; // Run process using (var processResults = await ProcessEx.RunAsync(processStartInfo, cancellationTokenSource.Token).ConfigureAwait(false)) { if (processResults.ExitCode == 1) { ImageProcessorBootstrapper.Instance.Logger.Log(typeof(PostProcessor), $"Unable to post process image for request {context.Request.Unvalidated.Url}, {processStartInfo.FileName} {processStartInfo.Arguments} exited with error code 1. Original image returned."); break; } } } } // Save result if (!File.Exists(destinationFile)) { File.Copy(sourceFile, destinationFile, true); } var result = new PostProcessingResultEventArgs(destinationFile, length); if (result.ResultFileSize > 0 && result.Saving > 0) { using (var fileStream = File.OpenRead(destinationFile)) { stream.SetLength(0); await fileStream.CopyToAsync(stream).ConfigureAwait(false); } } } } catch (OperationCanceledException) { ImageProcessorBootstrapper.Instance.Logger.Log(typeof(PostProcessor), $"Unable to post process image for request {context.Request.Unvalidated.Url} within {timeout}ms. Original image returned."); } catch (Exception ex) { // Some security policies don't allow execution of programs in this way ImageProcessorBootstrapper.Instance.Logger.Log(typeof(PostProcessor), ex.Message); } finally { // Always cleanup files try { // Ensure files exist, are not read only, and delete if (sourceFile != null && File.Exists(sourceFile)) { File.SetAttributes(sourceFile, FileAttributes.Normal); File.Delete(sourceFile); } if (destinationFile != null && File.Exists(destinationFile)) { File.SetAttributes(destinationFile, FileAttributes.Normal); File.Delete(destinationFile); } } catch { // Normally a no no, but logging would be excessive + temp files get cleaned up eventually. } } // ALways return stream (even if it's not optimized) stream.Position = 0; return(stream); }