Browse Source

Begin decoupling cache.

Former-commit-id: 282fdc30f4a813fe8fe759a21debfb6142676f52
Former-commit-id: 39a6b41dea2323461008da07120c387169913523
pull/17/head
James South 11 years ago
parent
commit
70073fe69a
  1. 98
      src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs
  2. 95
      src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj
  3. 36
      src/ImageProcessor.Web.AzureBlobCache/Properties/AssemblyInfo.cs
  4. 10
      src/ImageProcessor.Web.AzureBlobCache/packages.config
  5. 180
      src/ImageProcessor.Web/Caching/DiskCache2.cs
  6. 26
      src/ImageProcessor.Web/Caching/IImageCache.cs
  7. 125
      src/ImageProcessor.Web/Caching/ImageCacheBase.cs
  8. 32
      src/ImageProcessor.Web/Extensions/DirectoryInfoExtensions.cs
  9. 185
      src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs
  10. 4
      src/ImageProcessor.Web/ImageProcessor.Web.csproj
  11. 1
      src/packages/repositories.config

98
src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs

@ -0,0 +1,98 @@
namespace ImageProcessor.Web.AzureBlobCache
{
using System;
using System.Configuration;
using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Web.Caching;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
public class AzureBlobCache : ImageCacheBase
{
private CloudStorageAccount cloudStorageAccount;
private CloudBlobClient cloudBlobClient;
private CloudBlobContainer cloudBlobContainer;
public AzureBlobCache(string requestPath, string fullPath, string querystring)
: base(requestPath, fullPath, querystring)
{
// TODO: These should all be in the configuration.
// Retrieve storage account from connection string.
this.cloudStorageAccount = CloudStorageAccount.Parse(
CloudConfigurationManager.GetSetting("StorageConnectionString"));
// Create the blob client.
this.cloudBlobClient = this.cloudStorageAccount.CreateCloudBlobClient();
// Retrieve reference to a previously created container.
this.cloudBlobContainer = this.cloudBlobClient.GetContainerReference("mycontainer");
}
public override int MaxAge
{
get { throw new System.NotImplementedException(); }
}
public override async Task<bool> IsNewOrUpdatedAsync()
{
string cachedFileName = await this.CreateCachedFileName();
// TODO: Generate cache path.
CloudBlockBlob blockBlob = new CloudBlockBlob(new Uri(""));
bool isUpdated = false;
if (!await blockBlob.ExistsAsync())
{
// Nothing in the cache so we should return true.
isUpdated = true;
}
else if (blockBlob.Properties.LastModified.HasValue)
{
// Check to see if the cached image is set to expire.
if (this.IsExpired(blockBlob.Properties.LastModified.Value.UtcDateTime))
{
isUpdated = true;
}
}
return isUpdated;
}
public override async Task AddImageToCacheAsync(System.IO.Stream stream)
{
throw new System.NotImplementedException();
}
public override async Task TrimCacheAsync()
{
throw new System.NotImplementedException();
}
public override void RewritePath(HttpContext context)
{
throw new System.NotImplementedException();
}
/// <summary>
/// Gets a value indicating whether the given images creation date is out with
/// the prescribed limit.
/// </summary>
/// <param name="creationDate">
/// The creation date.
/// </param>
/// <returns>
/// The true if the date is out with the limit, otherwise; false.
/// </returns>
private bool IsExpired(DateTime creationDate)
{
return creationDate.AddDays(this.MaxAge) < DateTime.UtcNow.AddDays(-this.MaxAge);
}
}
}

95
src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ImageProcessor.Web.AzureBlobCache</RootNamespace>
<AssemblyName>ImageProcessor.Web.AzureBlobCache</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<RestorePackages>true</RestorePackages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Data.Edm">
<HintPath>..\packages\Microsoft.Data.Edm.5.6.2\lib\net40\Microsoft.Data.Edm.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Data.OData">
<HintPath>..\packages\Microsoft.Data.OData.5.6.2\lib\net40\Microsoft.Data.OData.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Data.Services.Client, Version=5.6.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Data.Services.Client.5.6.2\lib\net40\Microsoft.Data.Services.Client.dll</HintPath>
</Reference>
<Reference Include="Microsoft.WindowsAzure.Configuration">
<HintPath>..\packages\Microsoft.WindowsAzure.ConfigurationManager.1.8.0.0\lib\net35-full\Microsoft.WindowsAzure.Configuration.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Spatial, Version=5.6.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\System.Spatial.5.6.2\lib\net40\System.Spatial.dll</HintPath>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="AzureBlobCache.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ImageProcessor.Web\ImageProcessor.Web.csproj">
<Project>{d011a778-59c8-4bfa-a770-c350216bf161}</Project>
<Name>ImageProcessor.Web</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

36
src/ImageProcessor.Web.AzureBlobCache/Properties/AssemblyInfo.cs

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("ImageProcessor.Web.AzureBlobCache")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ImageProcessor.Web.AzureBlobCache")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("90605e94-25f4-4c69-b602-6b1df0adb89e")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

10
src/ImageProcessor.Web.AzureBlobCache/packages.config

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Data.Edm" version="5.6.2" targetFramework="net45" />
<package id="Microsoft.Data.OData" version="5.6.2" targetFramework="net45" />
<package id="Microsoft.Data.Services.Client" version="5.6.2" targetFramework="net45" />
<package id="Microsoft.WindowsAzure.ConfigurationManager" version="1.8.0.0" targetFramework="net45" />
<package id="Newtonsoft.Json" version="5.0.8" targetFramework="net45" />
<package id="System.Spatial" version="5.6.2" targetFramework="net45" />
<package id="WindowsAzure.Storage" version="4.3.0" targetFramework="net45" />
</packages>

180
src/ImageProcessor.Web/Caching/DiskCache2.cs

@ -0,0 +1,180 @@
namespace ImageProcessor.Web.Caching
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using ImageProcessor.Web.Configuration;
using ImageProcessor.Web.Extensions;
public class DiskCache2 : ImageCacheBase
{
/// <summary>
/// The maximum number of files allowed in the directory.
/// </summary>
/// <remarks>
/// NTFS directories can handle up to 10,000 files in the directory before slowing down.
/// This will help us to ensure that don't go over that limit.
/// <see href="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
/// <see href="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
/// <see href="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
/// </remarks>
private const int MaxFilesCount = 100;
/// <summary>
/// The virtual cache path.
/// </summary>
private static readonly string VirtualCachePath = ImageProcessorConfiguration.Instance.VirtualCachePath;
/// <summary>
/// The absolute path to virtual cache path on the server.
/// TODO: Change this so configuration is determined per IImageCache instance.
/// </summary>
private static readonly string AbsoluteCachePath = HostingEnvironment.MapPath(VirtualCachePath);
/// <summary>
/// The physical cached path.
/// </summary>
private string physicalCachedPath;
/// <summary>
/// The virtual cached path.
/// </summary>
private string virtualCachedPath;
public DiskCache2(string requestPath, string fullPath, string querystring)
: base(requestPath, fullPath, querystring)
{
}
/// <summary>
/// The maximum number of days to cache files on the system for.
/// TODO: Shift the getter source to proper config.
/// </summary>
public override int MaxAge
{
get
{
return ImageProcessorConfiguration.Instance.MaxCacheDays;
}
}
public override async Task<bool> IsNewOrUpdatedAsync()
{
string cachedFileName = await this.CreateCachedFileName();
// Collision rate of about 1 in 10000 for the folder structure.
// That gives us massive scope for files.
string pathFromKey = string.Join("\\", cachedFileName.ToCharArray().Take(6));
string virtualPathFromKey = pathFromKey.Replace(@"\", "/");
this.physicalCachedPath = Path.Combine(AbsoluteCachePath, pathFromKey, cachedFileName);
this.virtualCachedPath = Path.Combine(VirtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/");
this.CachedPath = this.physicalCachedPath;
bool isUpdated = false;
CachedImage cachedImage = CacheIndexer.GetValue(this.physicalCachedPath);
if (cachedImage == null)
{
// Nothing in the cache so we should return true.
isUpdated = true;
}
else
{
// Check to see if the cached image is set to expire.
if (this.IsExpired(cachedImage.CreationTimeUtc))
{
CacheIndexer.Remove(this.physicalCachedPath);
isUpdated = true;
}
}
return isUpdated;
}
public override async Task AddImageToCacheAsync(Stream stream)
{
// ReSharper disable once AssignNullToNotNullAttribute
DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.physicalCachedPath));
if (!directoryInfo.Exists)
{
directoryInfo.Create();
}
using (FileStream fileStream = File.Create(this.physicalCachedPath))
{
await stream.CopyToAsync(fileStream);
}
}
public override async Task TrimCacheAsync()
{
string directory = Path.GetDirectoryName(this.physicalCachedPath);
if (directory != null)
{
DirectoryInfo directoryInfo = new DirectoryInfo(directory);
DirectoryInfo parentDirectoryInfo = directoryInfo.Parent;
if (parentDirectoryInfo != null)
{
// UNC folders can throw exceptions if the file doesn't exist.
foreach (DirectoryInfo enumerateDirectory in await parentDirectoryInfo.SafeEnumerateDirectoriesAsync())
{
IEnumerable<FileInfo> files = enumerateDirectory.EnumerateFiles().OrderBy(f => f.CreationTimeUtc);
int count = files.Count();
foreach (FileInfo fileInfo in files)
{
try
{
// If the group count is equal to the max count minus 1 then we know we
// have reduced the number of items below the maximum allowed.
// We'll cleanup any orphaned expired files though.
if (!this.IsExpired(fileInfo.CreationTimeUtc) && count <= MaxFilesCount - 1)
{
break;
}
// Remove from the cache and delete each CachedImage.
CacheIndexer.Remove(fileInfo.Name);
fileInfo.Delete();
count -= 1;
}
// ReSharper disable once EmptyGeneralCatchClause
catch
{
// Do nothing; skip to the next file.
}
}
}
}
}
}
public override void RewritePath(HttpContext context)
{
// The cached file is valid so just rewrite the path.
context.RewritePath(this.virtualCachedPath, false);
}
/// <summary>
/// Gets a value indicating whether the given images creation date is out with
/// the prescribed limit.
/// </summary>
/// <param name="creationDate">
/// The creation date.
/// </param>
/// <returns>
/// The true if the date is out with the limit, otherwise; false.
/// </returns>
private bool IsExpired(DateTime creationDate)
{
return creationDate.AddDays(this.MaxAge) < DateTime.UtcNow.AddDays(-this.MaxAge);
}
}
}

26
src/ImageProcessor.Web/Caching/IImageCache.cs

@ -0,0 +1,26 @@

namespace ImageProcessor.Web.Caching
{
using System.IO;
using System.Threading.Tasks;
using System.Web;
public interface IImageCache
{
string CachedPath { get; }
int MaxAge { get; }
Task<bool> IsNewOrUpdatedAsync();
Task AddImageToCacheAsync(Stream stream);
Task TrimCacheAsync();
Task<string> CreateCachedFileName();
void RewritePath(HttpContext context);
//void SetHeaders(HttpContext context);
}
}

125
src/ImageProcessor.Web/Caching/ImageCacheBase.cs

@ -0,0 +1,125 @@
namespace ImageProcessor.Web.Caching
{
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Web.Extensions;
using ImageProcessor.Web.Helpers;
public abstract class ImageCacheBase : IImageCache
{
/// <summary>
/// The assembly version.
/// </summary>
private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
/// <summary>
/// The request path for the image.
/// </summary>
private readonly string requestPath;
/// <summary>
/// The full path for the image.
/// </summary>
private readonly string fullPath;
/// <summary>
/// The querystring containing processing instructions.
/// </summary>
private readonly string querystring;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCacheBase"/> class.
/// </summary>
/// <param name="requestPath">
/// The request path for the image.
/// </param>
/// <param name="fullPath">
/// The full path for the image.
/// </param>
/// <param name="querystring">
/// The querystring containing instructions.
/// </param>
protected ImageCacheBase(string requestPath, string fullPath, string querystring)
{
this.requestPath = requestPath;
this.fullPath = fullPath;
this.querystring = querystring;
}
public string CachedPath { get; protected set; }
public abstract int MaxAge { get; }
public abstract Task<bool> IsNewOrUpdatedAsync();
public abstract Task AddImageToCacheAsync(Stream stream);
public abstract Task TrimCacheAsync();
public Task<string> CreateCachedFileName()
{
string streamHash = string.Empty;
try
{
if (new Uri(this.requestPath).IsFile)
{
// Get the hash for the filestream. That way we can ensure that if the image is
// updated but has the same name we will know.
FileInfo imageFileInfo = new FileInfo(this.requestPath);
if (imageFileInfo.Exists)
{
// Pull the latest info.
imageFileInfo.Refresh();
// Checking the stream itself is far too processor intensive so we make a best guess.
string creation = imageFileInfo.CreationTimeUtc.ToString(CultureInfo.InvariantCulture);
string length = imageFileInfo.Length.ToString(CultureInfo.InvariantCulture);
streamHash = string.Format("{0}{1}", creation, length);
}
}
}
catch
{
streamHash = string.Empty;
}
// Use an sha1 hash of the full path including the querystring to create the image name.
// That name can also be used as a key for the cached image and we should be able to use
// The characters of that hash as sub-folders.
string parsedExtension = ImageHelpers.GetExtension(this.fullPath, this.querystring);
string encryptedName = (streamHash + this.fullPath).ToSHA1Fingerprint();
string cachedFileName = string.Format(
"{0}.{1}",
encryptedName,
!string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension.Replace(".", string.Empty) : "jpg");
this.CachedPath = cachedFileName;
return Task.FromResult(cachedFileName);
}
public abstract void RewritePath(HttpContext context);
public virtual void SetHeaders(HttpContext context, string responseType)
{
HttpResponse response = context.Response;
response.ContentType = responseType;
if (response.Headers["Image-Served-By"] == null)
{
response.AddHeader("Image-Served-By", "ImageProcessor.Web/" + AssemblyVersion);
}
HttpCachePolicy cache = response.Cache;
cache.SetCacheability(HttpCacheability.Public);
cache.VaryByHeaders["Accept-Encoding"] = true;
}
}
}

32
src/ImageProcessor.Web/Extensions/DirectoryInfoExtensions.cs

@ -13,6 +13,7 @@ namespace ImageProcessor.Web.Extensions
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
/// <summary>
/// Provides extension methods to the <see cref="System.IO.DirectoryInfo"/> type.
@ -37,7 +38,36 @@ namespace ImageProcessor.Web.Extensions
/// <returns>
/// An enumerable collection of directories that matches searchPattern and searchOption.
/// </returns>
public static IEnumerable<DirectoryInfo> SafeEnumerateDirectories(this DirectoryInfo directoryInfo, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
public static Task<IEnumerable<DirectoryInfo>> SafeEnumerateDirectoriesAsync(
this DirectoryInfo directoryInfo,
string searchPattern = "*",
SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return Task.Run(() => SafeEnumerateDirectories(directoryInfo, searchPattern, searchOption));
}
/// <summary>
/// Returns an enumerable collection of directory information that matches a specified search pattern and search subdirectory option.
/// Will return an empty enumerable on exception. Quick and dirty but does what I need just now.
/// </summary>
/// <param name="directoryInfo">
/// The <see cref="System.IO.DirectoryInfo"/> that this method extends.
/// </param>
/// <param name="searchPattern">
/// The search string to match against the names of directories. This parameter can contain a combination of valid literal path
/// and wildcard (* and ?) characters (see Remarks), but doesn't support regular expressions. The default pattern is "*", which returns all files.
/// </param>
/// <param name="searchOption">
/// One of the enumeration values that specifies whether the search operation should include only
/// the current directory or all subdirectories. The default value is TopDirectoryOnly.
/// </param>
/// <returns>
/// An enumerable collection of directories that matches searchPattern and searchOption.
/// </returns>
public static IEnumerable<DirectoryInfo> SafeEnumerateDirectories(
this DirectoryInfo directoryInfo,
string searchPattern = "*",
SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
IEnumerable<DirectoryInfo> directories;

185
src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs

@ -85,6 +85,8 @@ namespace ImageProcessor.Web.HttpModules
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
private IImageCache imageCache;
#endregion
#region Destructors
@ -222,7 +224,7 @@ namespace ImageProcessor.Web.HttpModules
/// <returns>
/// The <see cref="T:System.Threading.Tasks.Task"/>.
/// </returns>
private Task PostProcessImage(object sender, EventArgs e)
private async Task PostProcessImage(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)sender).Context;
object cachedPathObject = context.Items[CachedPathKey];
@ -232,18 +234,16 @@ namespace ImageProcessor.Web.HttpModules
string cachedPath = cachedPathObject.ToString();
// Trim the cache.
DiskCache.TrimCachedFolders(cachedPath);
await this.imageCache.TrimCacheAsync();
// Fire the post processing event.
EventHandler<PostProcessingEventArgs> handler = OnPostProcessing;
if (handler != null)
{
context.Items[CachedPathKey] = null;
return Task.Run(() => handler(this, new PostProcessingEventArgs { CachedImagePath = cachedPath }));
await Task.Run(() => handler(this, new PostProcessingEventArgs { CachedImagePath = cachedPath }));
}
}
return Task.FromResult<object>(null);
}
/// <summary>
@ -258,17 +258,11 @@ namespace ImageProcessor.Web.HttpModules
object responseTypeObject = context.Items[CachedResponseTypeKey];
object dependencyFileObject = context.Items[CachedResponseFileDependency];
if (responseTypeObject != null && dependencyFileObject != null)
{
string responseType = (string)responseTypeObject;
List<string> dependencyFiles = (List<string>)dependencyFileObject;
// Set the headers
this.SetHeaders(context, responseType, dependencyFiles);
string responseType = responseTypeObject as string;
List<string> dependencyFiles = dependencyFileObject as List<string>;
context.Items[CachedResponseTypeKey] = null;
context.Items[CachedResponseFileDependency] = null;
}
// Set the headers
this.SetHeaders(context, responseType, dependencyFiles);
}
#region Private
@ -379,100 +373,83 @@ namespace ImageProcessor.Web.HttpModules
}
// Create a new cache to help process and cache the request.
DiskCache cache = new DiskCache(requestPath, fullPath, queryString);
string cachedPath = cache.CachedPath;
// Since we are now rewriting the path we need to check again that the current user has access
// to the rewritten path.
// Get the user for the current request
// If the user is anonymous or authentication doesn't work for this suffix avoid a NullReferenceException
// in the UrlAuthorizationModule by creating a generic identity.
string virtualCachedPath = cache.VirtualCachedPath;
this.imageCache = new DiskCache2(requestPath, fullPath, queryString);
IPrincipal user = context.User ?? new GenericPrincipal(new GenericIdentity(string.Empty, string.Empty), new string[0]);
// Is the file new or updated?
bool isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync();
string cachedPath = this.imageCache.CachedPath;
// Do we have permission to call UrlAuthorizationModule.CheckUrlAccessForPrincipal?
PermissionSet permission = new PermissionSet(PermissionState.None);
permission.AddPermission(new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted));
bool hasPermission = permission.IsSubsetOf(AppDomain.CurrentDomain.PermissionSet);
bool isAllowed = true;
// Run the rewritten path past the authorization system again.
// We can then use the result as the default "AllowAccess" value
if (hasPermission && !context.SkipAuthorization)
{
isAllowed = UrlAuthorizationModule.CheckUrlAccessForPrincipal(virtualCachedPath, user, "GET");
}
if (isAllowed)
// Only process if the file has been updated.
if (isNewOrUpdated)
{
// Is the file new or updated?
bool isNewOrUpdated = cache.IsNewOrUpdatedFile(cachedPath);
// Only process if the file has been updated.
if (isNewOrUpdated)
// Process the image.
using (ImageFactory imageFactory = new ImageFactory(preserveExifMetaData != null && preserveExifMetaData.Value))
{
// Process the image.
using (ImageFactory imageFactory = new ImageFactory(preserveExifMetaData != null && preserveExifMetaData.Value))
using (await this.locker.LockAsync(cachedPath))
{
using (await this.locker.LockAsync(cachedPath))
{
byte[] imageBuffer = await currentService.GetImage(resourcePath);
byte[] imageBuffer = await currentService.GetImage(resourcePath);
using (MemoryStream memoryStream = new MemoryStream(imageBuffer))
{
// Reset the position of the stream to ensure we're reading the correct part.
memoryStream.Position = 0;
using (MemoryStream memoryStream = new MemoryStream(imageBuffer))
{
// Reset the position of the stream to ensure we're reading the correct part.
memoryStream.Position = 0;
// Process the Image
imageFactory.Load(memoryStream).AutoProcess(queryString).Save(cachedPath);
// Process the Image
imageFactory.Load(memoryStream).AutoProcess(queryString).Save(memoryStream);
memoryStream.Position = 0;
// Add to the cache.
cache.AddImageToCache(cachedPath);
// Add to the cache.
await this.imageCache.AddImageToCacheAsync(memoryStream);
// Store the cached path, response type, and cache dependency in the context for later retrieval.
context.Items[CachedPathKey] = cachedPath;
context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType;
// Store the cached path, response type, and cache dependency in the context for later retrieval.
context.Items[CachedPathKey] = cachedPath;
context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType;
if (isFileLocal)
{
// Some services might only provide filename so we can't monitor for the browser.
context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath
? new List<string> { cachedPath }
: new List<string> { requestPath, cachedPath };
}
else
{
context.Items[CachedResponseFileDependency] = new List<string> { cachedPath };
}
}
}
}
}
// Image is from the cache so the mime-type will need to be set.
if (context.Items[CachedResponseTypeKey] == null)
{
string mimetype = ImageHelpers.GetMimeType(cachedPath);
if (!string.IsNullOrEmpty(mimetype))
{
context.Items[CachedResponseTypeKey] = mimetype;
}
}
// Image is from the cache so the mime-type will need to be set.
// TODO: Is this bit needed? Is the static file handler doing stuff for the filecache
// but not others.
if (context.Items[CachedResponseTypeKey] == null)
{
string mimetype = ImageHelpers.GetMimeType(this.imageCache.CachedPath);
if (context.Items[CachedResponseFileDependency] == null)
if (!string.IsNullOrEmpty(mimetype))
{
if (isFileLocal)
{
// Some services might only provide filename so we can't monitor for the browser.
context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath
? new List<string> { cachedPath }
: new List<string> { requestPath, cachedPath };
}
else
{
context.Items[CachedResponseFileDependency] = new List<string> { cachedPath };
}
context.Items[CachedResponseTypeKey] = mimetype;
}
// The cached file is valid so just rewrite the path.
context.RewritePath(virtualCachedPath, false);
}
else
if (context.Items[CachedResponseFileDependency] == null)
{
throw new HttpException(403, "Access denied");
if (isFileLocal)
{
// Some services might only provide filename so we can't monitor for the browser.
context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath
? new List<string> { this.imageCache.CachedPath }
: new List<string> { requestPath, this.imageCache.CachedPath };
}
else
{
context.Items[CachedResponseFileDependency] = new List<string> { this.imageCache.CachedPath };
}
}
// The cached file is valid so just rewrite the path.
this.imageCache.RewritePath(context);
}
}
@ -494,28 +471,34 @@ namespace ImageProcessor.Web.HttpModules
{
HttpResponse response = context.Response;
response.ContentType = responseType;
if (response.Headers["Image-Served-By"] == null)
{
response.AddHeader("Image-Served-By", "ImageProcessor.Web/" + AssemblyVersion);
}
HttpCachePolicy cache = response.Cache;
cache.SetCacheability(HttpCacheability.Public);
cache.VaryByHeaders["Accept-Encoding"] = true;
if (this.imageCache != null)
{
HttpCachePolicy cache = response.Cache;
cache.SetCacheability(HttpCacheability.Public);
cache.VaryByHeaders["Accept-Encoding"] = true;
context.Response.AddFileDependencies(dependencyPaths.ToArray());
cache.SetLastModifiedFromFileDependencies();
if (!string.IsNullOrWhiteSpace(responseType))
{
response.ContentType = responseType;
}
int maxDays = DiskCache.MaxFileCachedDuration;
if (dependencyPaths != null)
{
context.Response.AddFileDependencies(dependencyPaths.ToArray());
cache.SetLastModifiedFromFileDependencies();
}
cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(maxDays));
cache.SetMaxAge(new TimeSpan(maxDays, 0, 0, 0));
cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
int maxDays = this.imageCache.MaxAge;
context.Items[CachedResponseTypeKey] = null;
context.Items[CachedResponseFileDependency] = null;
cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(maxDays));
cache.SetMaxAge(new TimeSpan(maxDays, 0, 0, 0));
cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
}
}
/// <summary>

4
src/ImageProcessor.Web/ImageProcessor.Web.csproj

@ -46,6 +46,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Caching\CachedImage.cs" />
<Compile Include="Caching\DiskCache2.cs" />
<Compile Include="Caching\IImageCache.cs" />
<Compile Include="Caching\ImageCacheBase.cs" />
<Compile Include="Helpers\ProcessQueryStringEventArgs.cs" />
<Compile Include="Processors\DetectEdges.cs" />
<Compile Include="Processors\EntropyCrop.cs" />
@ -56,7 +59,6 @@
<Compile Include="Processors\ReplaceColor.cs" />
<Compile Include="Services\IImageService.cs" />
<Compile Include="Caching\MemCache.cs" />
<Compile Include="Caching\DiskCache.cs" />
<Compile Include="Caching\CacheIndexer.cs" />
<Compile Include="Configuration\ImageCacheSection.cs" />
<Compile Include="Configuration\ImageProcessingSection.cs" />

1
src/packages/repositories.config

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<repositories>
<repository path="..\ImageProcessor.UnitTests\packages.config" />
<repository path="..\ImageProcessor.Web.AzureBlobCache\packages.config" />
<repository path="..\ImageProcessor.Web.UnitTests\packages.config" />
<repository path="..\TestWebsites\MVC\packages.config" />
</repositories>
Loading…
Cancel
Save