Browse Source

Merge remote-tracking branch 'origin/feature/issue/120' into V2

Former-commit-id: 68a3393b7f2e8629a85286303632cd6e4bd5d1f2
Former-commit-id: a6bd2be80b37ac875947edfdbafaf21506208e65
pull/17/head
James South 11 years ago
parent
commit
5bcc3edf00
  1. 6
      build/build.xml
  2. 8
      src/ImageProcessor.Playground/ImageProcessor.Playground.csproj
  3. 5
      src/ImageProcessor.Playground/Program.cs
  4. 346
      src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs
  5. 99
      src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj
  6. 36
      src/ImageProcessor.Web.AzureBlobCache/Properties/AssemblyInfo.cs
  7. 10
      src/ImageProcessor.Web.AzureBlobCache/packages.config
  8. 5
      src/ImageProcessor.Web.PostProcessor/PostProcessor.cs
  9. 4
      src/ImageProcessor.Web.PostProcessor/Properties/AssemblyInfo.cs
  10. 8
      src/ImageProcessor.Web.PostProcessor/README.md
  11. 45
      src/ImageProcessor.Web/Caching/CacheIndexer.cs
  12. 2
      src/ImageProcessor.Web/Caching/CachedImage.cs
  13. 304
      src/ImageProcessor.Web/Caching/DiskCache.cs
  14. 84
      src/ImageProcessor.Web/Caching/IImageCache.cs
  15. 182
      src/ImageProcessor.Web/Caching/ImageCacheBase.cs
  16. 157
      src/ImageProcessor.Web/Configuration/ImageCacheSection.cs
  17. 156
      src/ImageProcessor.Web/Configuration/ImageProcessingSection.cs
  18. 72
      src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs
  19. 171
      src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs
  20. 11
      src/ImageProcessor.Web/Configuration/Resources/cache.config
  21. 1
      src/ImageProcessor.Web/Configuration/Resources/security.config
  22. 56
      src/ImageProcessor.Web/Configuration/Shared/SettingElement.cs
  23. 117
      src/ImageProcessor.Web/Configuration/Shared/SettingElementCollection.cs
  24. 32
      src/ImageProcessor.Web/Extensions/DirectoryInfoExtensions.cs
  25. 180
      src/ImageProcessor.Web/Extensions/TypeInitializationExtensions.cs
  26. 26
      src/ImageProcessor.Web/Helpers/ImageHelpers.cs
  27. 11
      src/ImageProcessor.Web/Helpers/RemoteFile.cs
  28. 19
      src/ImageProcessor.Web/Helpers/TypePropertyHelpers.cs
  29. 230
      src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs
  30. 8
      src/ImageProcessor.Web/ImageProcessor.Web.csproj
  31. 2
      src/ImageProcessor.Web/ImageProcessor.Web.csproj.DotSettings
  32. 4
      src/ImageProcessor.Web/Services/IImageService.cs
  33. 5
      src/ImageProcessor.Web/Services/RemoteImageService.cs
  34. 1
      src/ImageProcessor.Web/Settings.StyleCop
  35. 17
      src/ImageProcessor.sln
  36. 1
      src/ImageProcessor.sln.DotSettings
  37. 6
      src/ImageProcessor/ImageFactory.cs
  38. 1
      src/ImageProcessor/Imaging/Formats/FormatBase.cs
  39. 18
      src/TestWebsites/MVC/Global.asax.cs
  40. 4
      src/TestWebsites/MVC/Test_Website_MVC.csproj
  41. 19
      src/TestWebsites/MVC/Views/Home/Index.cshtml
  42. 4
      src/TestWebsites/MVC/Web.config
  43. 12
      src/TestWebsites/MVC/config/imageprocessor/cache.config
  44. 2
      src/TestWebsites/MVC/config/imageprocessor/security.config
  45. 1
      src/packages/repositories.config

6
build/build.xml

@ -13,7 +13,7 @@
<project>
<name>ImageProcessor Web</name>
<version>4.1.5.0</version>
<version>4.2.0.0</version>
<folder>..\src\ImageProcessor.Web</folder>
<projfile>ImageProcessor.Web.csproj</projfile>
<outputs>
@ -24,7 +24,7 @@
<project>
<name>ImageProcessor Web PostProcessor</name>
<version>1.0.1.0</version>
<version>1.0.2.0</version>
<folder>..\src\ImageProcessor.Web.PostProcessor</folder>
<projfile>ImageProcessor.Web.PostProcessor.csproj</projfile>
<outputs>
@ -35,7 +35,7 @@
<project>
<name>ImageProcessor Web.config sample</name>
<version>2.1.1.0</version>
<version>2.2.0.0</version>
<nuspec>ImageProcessor.Web.Config.nuspec</nuspec>
</project>

8
src/ImageProcessor.Playground/ImageProcessor.Playground.csproj

@ -55,6 +55,14 @@
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ImageProcessor.Web.AzureBlobCache\ImageProcessor.Web.AzureBlobCache.csproj">
<Project>{3c805e4c-d679-43f8-8c43-8909cdb4d4d7}</Project>
<Name>ImageProcessor.Web.AzureBlobCache</Name>
</ProjectReference>
<ProjectReference Include="..\ImageProcessor.Web\ImageProcessor.Web.csproj">
<Project>{d011a778-59c8-4bfa-a770-c350216bf161}</Project>
<Name>ImageProcessor.Web</Name>
</ProjectReference>
<ProjectReference Include="..\ImageProcessor\ImageProcessor.csproj">
<Project>{3B5DD734-FB7A-487D-8CE6-55E7AF9AEA7E}</Project>
<Name>ImageProcessor</Name>

5
src/ImageProcessor.Playground/Program.cs

@ -24,6 +24,7 @@ namespace ImageProcessor.PlayGround
using ImageProcessor.Imaging.Filters.Photo;
using ImageProcessor.Imaging.Formats;
using ImageProcessor.Processors;
using ImageProcessor.Web.Caching;
/// <summary>
/// The program.
@ -38,6 +39,10 @@ namespace ImageProcessor.PlayGround
/// </param>
public static void Main(string[] args)
{
var x = typeof(AzureBlobCache);
Console.WriteLine(x.AssemblyQualifiedName);
Console.ReadLine();
string path = new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath;
// ReSharper disable once AssignNullToNotNullAttribute

346
src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs

@ -0,0 +1,346 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AzureBlobCache.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Provides an <see cref="IImageCache" /> implementation that uses Azure blob storage.
// The cache is self healing and cleaning.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Caching
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Web.Extensions;
using ImageProcessor.Web.Helpers;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
/// <summary>
/// Provides an <see cref="IImageCache"/> implementation that uses Azure blob storage.
/// The cache is self healing and cleaning.
/// </summary>
public class AzureBlobCache : ImageCacheBase
{
/// <summary>
/// The maximum number of days to store the image.
/// </summary>
private readonly int maxDays;
/// <summary>
/// The cloud cached blob container.
/// </summary>
private readonly CloudBlobContainer cloudCachedBlobContainer;
/// <summary>
/// The cloud source blob container.
/// </summary>
private readonly CloudBlobContainer cloudSourceBlobContainer;
/// <summary>
/// The cached root url for a content delivery network.
/// </summary>
private readonly string cachedCdnRoot;
/// <summary>
/// The cached rewrite path.
/// </summary>
private string cachedRewritePath;
/// <summary>
/// Initializes a new instance of the <see cref="AzureBlobCache"/> 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>
public AzureBlobCache(string requestPath, string fullPath, string querystring)
: base(requestPath, fullPath, querystring)
{
this.maxDays = Convert.ToInt32(this.Settings["MaxDays"]);
// Retrieve storage accounts from connection string.
CloudStorageAccount cloudCachedStorageAccount = CloudStorageAccount.Parse(this.Settings["CachedStorageAccount"]);
CloudStorageAccount cloudSourceStorageAccount = CloudStorageAccount.Parse(this.Settings["SourceStorageAccount"]);
// Create the blob clients.
CloudBlobClient cloudCachedBlobClient = cloudCachedStorageAccount.CreateCloudBlobClient();
CloudBlobClient cloudSourceBlobClient = cloudSourceStorageAccount.CreateCloudBlobClient();
// Retrieve references to a previously created containers.
this.cloudCachedBlobContainer = cloudCachedBlobClient.GetContainerReference(this.Settings["CachedBlobContainer"]);
this.cloudSourceBlobContainer = cloudSourceBlobClient.GetContainerReference(this.Settings["SourceBlobContainer"]);
this.cachedCdnRoot = this.Settings["CachedCDNRoot"];
}
/// <summary>
/// Gets the maximum number of days to store the image.
/// </summary>
public override int MaxDays
{
get
{
return this.maxDays;
}
}
/// <summary>
/// Gets a value indicating whether the image is new or updated in an asynchronous manner.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> returning the value.
/// </returns>
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 to store millions of files.
string pathFromKey = string.Join("\\", cachedFileName.ToCharArray().Take(6));
this.CachedPath = Path.Combine(this.cloudCachedBlobContainer.Uri.ToString(), pathFromKey, cachedFileName).Replace(@"\", "/");
this.cachedRewritePath = Path.Combine(this.cachedCdnRoot, this.cloudCachedBlobContainer.Name, pathFromKey, cachedFileName).Replace(@"\", "/");
bool isUpdated = false;
CachedImage cachedImage = CacheIndexer.GetValue(this.CachedPath);
if (new Uri(this.CachedPath).IsFile)
{
FileInfo fileInfo = new FileInfo(this.CachedPath);
if (fileInfo.Exists)
{
// Pull the latest info.
fileInfo.Refresh();
cachedImage = new CachedImage
{
Key = Path.GetFileNameWithoutExtension(this.CachedPath),
Path = this.CachedPath,
CreationTimeUtc = fileInfo.CreationTimeUtc
};
CacheIndexer.Add(cachedImage);
}
}
if (cachedImage == null)
{
string blobPath = this.CachedPath.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1);
CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(blobPath);
if (await blockBlob.ExistsAsync())
{
// Pull the latest info.
await blockBlob.FetchAttributesAsync();
if (blockBlob.Properties.LastModified.HasValue)
{
cachedImage = new CachedImage
{
Key = Path.GetFileNameWithoutExtension(this.CachedPath),
Path = this.CachedPath,
CreationTimeUtc =
blockBlob.Properties.LastModified.Value.UtcDateTime
};
CacheIndexer.Add(cachedImage);
}
}
}
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.CachedPath);
isUpdated = true;
}
}
return isUpdated;
}
/// <summary>
/// Adds the image to the cache in an asynchronous manner.
/// </summary>
/// <param name="stream">
/// The stream containing the image data.
/// </param>
/// <param name="contentType">
/// The content type of the image.
/// </param>
/// <returns>
/// The <see cref="Task"/> representing an asynchronous operation.
/// </returns>
public override async Task AddImageToCacheAsync(Stream stream, string contentType)
{
string blobPath = this.CachedPath.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1);
CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(blobPath);
await blockBlob.UploadFromStreamAsync(stream);
blockBlob.Properties.ContentType = contentType;
blockBlob.Properties.CacheControl = string.Format("public, max-age={0}", this.MaxDays * 86400);
await blockBlob.SetPropertiesAsync();
}
/// <summary>
/// Trims the cache of any expired items in an asynchronous manner.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> representing an asynchronous operation.
/// </returns>
public override async Task TrimCacheAsync()
{
Uri uri = new Uri(this.CachedPath);
string path = uri.GetLeftPart(UriPartial.Path).Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1);
string directory = path.Substring(0, path.LastIndexOf('/'));
string parent = directory.Substring(0, directory.LastIndexOf('/'));
BlobContinuationToken continuationToken = null;
List<IListBlobItem> results = new List<IListBlobItem>();
// Loop through the all the files in a non blocking fashion.
do
{
BlobResultSegment response = await this.cloudCachedBlobContainer
.ListBlobsSegmentedAsync(parent, true, BlobListingDetails.Metadata, 5000, continuationToken, null, null);
continuationToken = response.ContinuationToken;
results.AddRange(response.Results);
}
while (continuationToken != null);
// Now leap through and delete.
foreach (CloudBlockBlob blob in results
.Where((blobItem, type) => blobItem is CloudBlockBlob)
.Cast<CloudBlockBlob>()
.OrderBy(b => b.Properties.LastModified != null ? b.Properties.LastModified.Value.UtcDateTime : new DateTime()))
{
if (blob.Properties.LastModified.HasValue
&& !this.IsExpired(blob.Properties.LastModified.Value.UtcDateTime))
{
break;
}
// Remove from the cache and delete each CachedImage.
CacheIndexer.Remove(blob.Name);
await blob.DeleteAsync();
}
}
/// <summary>
/// Gets a string identifying the cached file name.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> returning the value.
/// </returns>
public override async 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);
}
}
else
{
Regex regex = new Regex("^http(s)?://");
string container = regex.Replace(this.cloudSourceBlobContainer.Uri.ToString(), string.Empty);
string blobPath = regex.Replace(this.RequestPath, string.Empty);
blobPath = blobPath.Replace(container, string.Empty).TrimStart('/');
CloudBlockBlob blockBlob = this.cloudSourceBlobContainer.GetBlockBlobReference(blobPath);
if (await blockBlob.ExistsAsync())
{
// Pull the latest info.
await blockBlob.FetchAttributesAsync();
if (blockBlob.Properties.LastModified.HasValue)
{
string creation = blockBlob.Properties
.LastModified.Value.UtcDateTime
.ToString(CultureInfo.InvariantCulture);
string length = blockBlob.Properties.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");
return cachedFileName;
}
/// <summary>
/// Rewrites the path to point to the cached image.
/// </summary>
/// <param name="context">
/// The <see cref="HttpContext"/> encapsulating all information about the request.
/// </param>
public override void RewritePath(HttpContext context)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.cachedRewritePath);
request.Method = "HEAD";
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
HttpStatusCode responseCode = response.StatusCode;
context.Response.Redirect(
responseCode == HttpStatusCode.NotFound ? this.CachedPath : this.cachedRewritePath,
false);
}
}
}
}

99
src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj

@ -0,0 +1,99 @@
<?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.Caching</RootNamespace>
<AssemblyName>ImageProcessor.Web.Caching.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="Microsoft.WindowsAzure.Storage">
<HintPath>..\packages\WindowsAzure.Storage.4.3.0\lib\net40\Microsoft.WindowsAzure.Storage.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.Configuration" />
<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>

5
src/ImageProcessor.Web.PostProcessor/PostProcessor.cs

@ -34,6 +34,11 @@ namespace ImageProcessor.Web.PostProcessor
/// </returns>
public static async Task PostProcessImageAsync(string sourceFile)
{
if (!new Uri(sourceFile).IsFile)
{
return;
}
string targetFile = Path.GetTempFileName();
PostProcessingResultEventArgs result = await RunProcess(sourceFile, targetFile);

4
src/ImageProcessor.Web.PostProcessor/Properties/AssemblyInfo.cs

@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// 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.1.0")]
[assembly: AssemblyVersion("1.0.1.0")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyVersion("1.0.2.0")]
[assembly: AssemblyFileVersion("1.0.2.0")]

8
src/ImageProcessor.Web.PostProcessor/README.md

@ -1,13 +1,13 @@
#Resource locations
###gifsicle
http://www.lcdf.org/gifsicle/
[http://www.lcdf.org/gifsicle/](http://www.lcdf.org/gifsicle/)
###jpegtran
http://jpegclub.org/jpegtran/
[http://jpegclub.org/jpegtran/](http://jpegclub.org/jpegtran/)
###optipng
http://optipng.sourceforge.net/
[http://optipng.sourceforge.net/](http://optipng.sourceforge.net/)
###pngout
http://advsys.net/ken/utils.htm
[http://advsys.net/ken/utils.htm](http://advsys.net/ken/utils.htm)

45
src/ImageProcessor.Web/Caching/CacheIndexer.cs

@ -10,14 +10,15 @@
namespace ImageProcessor.Web.Caching
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Caching;
/// <summary>
/// Represents an in memory collection of keys and values whose operations are concurrent.
/// Represents an in memory collection of cached images whose operations are concurrent.
/// </summary>
internal static class CacheIndexer
public static class CacheIndexer
{
#region Public
/// <summary>
@ -34,30 +35,6 @@ namespace ImageProcessor.Web.Caching
{
string key = Path.GetFileNameWithoutExtension(cachedPath);
CachedImage cachedImage = (CachedImage)MemCache.GetItem(key);
if (cachedImage == null)
{
// FileInfo is thread safe.
FileInfo fileInfo = new FileInfo(cachedPath);
if (!fileInfo.Exists)
{
return null;
}
// Pull the latest info.
fileInfo.Refresh();
cachedImage = new CachedImage
{
Key = Path.GetFileNameWithoutExtension(cachedPath),
Path = cachedPath,
CreationTimeUtc = fileInfo.CreationTimeUtc
};
Add(cachedImage);
}
return cachedImage;
}
@ -88,11 +65,19 @@ namespace ImageProcessor.Web.Caching
/// </returns>
public static CachedImage Add(CachedImage cachedImage)
{
// Add the CachedImage.
CacheItemPolicy policy = new CacheItemPolicy();
policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string> { cachedImage.Path }));
if (new Uri(cachedImage.Path).IsFile)
{
// Add the CachedImage.
CacheItemPolicy policy = new CacheItemPolicy();
policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string> { cachedImage.Path }));
MemCache.AddItem(Path.GetFileNameWithoutExtension(cachedImage.Key), cachedImage, policy);
}
else
{
MemCache.AddItem(Path.GetFileNameWithoutExtension(cachedImage.Key), cachedImage);
}
MemCache.AddItem(cachedImage.Key, cachedImage, policy);
return cachedImage;
}
#endregion

2
src/ImageProcessor.Web/Caching/CachedImage.cs

@ -15,7 +15,7 @@ namespace ImageProcessor.Web.Caching
/// <summary>
/// Describes a cached image
/// </summary>
internal sealed class CachedImage
public sealed class CachedImage
{
/// <summary>
/// Gets or sets the key identifying the cached image.

304
src/ImageProcessor.Web/Caching/DiskCache.cs

@ -1,39 +1,33 @@
// --------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DiskCache.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// The disk cache.
// Provides an <see cref="IImageCache" /> implementation that is file system based.
// The cache is self healing and cleaning.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Caching
{
#region Using
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Configuration;
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;
using ImageProcessor.Web.Helpers;
#endregion
/// <summary>
/// The disk cache.
/// Provides an <see cref="IImageCache"/> implementation that is file system based.
/// The cache is self healing and cleaning.
/// </summary>
internal sealed class DiskCache
public class DiskCache : ImageCacheBase
{
#region Fields
/// <summary>
/// The maximum number of days to cache files on the system for.
/// </summary>
internal static readonly int MaxFileCachedDuration = ImageProcessorConfiguration.Instance.MaxCacheDays;
/// <summary>
/// The maximum number of files allowed in the directory.
/// </summary>
@ -47,42 +41,25 @@ namespace ImageProcessor.Web.Caching
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.
/// </summary>
private static readonly string AbsoluteCachePath = HostingEnvironment.MapPath(ImageProcessorConfiguration.Instance.VirtualCachePath);
/// <summary>
/// The request path for the image.
/// The maximum number of days to store the image.
/// </summary>
private readonly string requestPath;
private readonly int maxDays;
/// <summary>
/// The full path for the image.
/// </summary>
private readonly string fullPath;
/// <summary>
/// The querystring containing processing instructions.
/// The virtual cache path.
/// </summary>
private readonly string querystring;
private readonly string virtualCachePath;
/// <summary>
/// The physical cached path.
/// The absolute path to virtual cache path on the server.
/// </summary>
private string physicalCachedPath;
private readonly string absoluteCachePath;
/// <summary>
/// The virtual cached path.
/// The virtual path to the cached file.
/// </summary>
private string virtualCachedPath;
#endregion
private string virtualCachedFilePath;
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="DiskCache"/> class.
/// </summary>
@ -96,49 +73,126 @@ namespace ImageProcessor.Web.Caching
/// The querystring containing instructions.
/// </param>
public DiskCache(string requestPath, string fullPath, string querystring)
: base(requestPath, fullPath, querystring)
{
this.requestPath = requestPath;
this.fullPath = fullPath;
this.querystring = querystring;
this.maxDays = Convert.ToInt32(this.Settings["MaxDays"]);
string virtualPath = this.Settings["VirtualCachePath"];
if (!virtualPath.IsValidVirtualPathName())
{
throw new ConfigurationErrorsException("DiskCache 'VirtualCachePath' is not a valid virtual path.");
}
// Get the physical and virtual paths.
this.GetCachePaths();
this.virtualCachePath = virtualPath;
this.absoluteCachePath = HostingEnvironment.MapPath(this.virtualCachePath);
}
#endregion
/// <summary>
/// Gets the cached path.
/// Gets the maximum number of days to store the image.
/// </summary>
public string CachedPath
public override int MaxDays
{
get
{
return this.physicalCachedPath;
return this.maxDays;
}
}
/// <summary>
/// Gets the cached path.
/// Gets a value indicating whether the image is new or updated in an asynchronous manner.
/// </summary>
public string VirtualCachedPath
/// <returns>
/// The <see cref="Task"/>.
/// </returns>
public override async Task<bool> IsNewOrUpdatedAsync()
{
get
string cachedFileName = await this.CreateCachedFileName();
// Collision rate of about 1 in 10000 for the folder structure.
// That gives us massive scope to store millions of files.
string pathFromKey = string.Join("\\", cachedFileName.ToCharArray().Take(6));
string virtualPathFromKey = pathFromKey.Replace(@"\", "/");
this.CachedPath = Path.Combine(this.absoluteCachePath, pathFromKey, cachedFileName);
this.virtualCachedFilePath = Path.Combine(this.virtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/");
bool isUpdated = false;
CachedImage cachedImage = CacheIndexer.GetValue(this.CachedPath);
if (cachedImage == null)
{
return this.virtualCachedPath;
FileInfo fileInfo = new FileInfo(this.CachedPath);
if (fileInfo.Exists)
{
// Pull the latest info.
fileInfo.Refresh();
cachedImage = new CachedImage
{
Key = Path.GetFileNameWithoutExtension(this.CachedPath),
Path = this.CachedPath,
CreationTimeUtc = fileInfo.CreationTimeUtc
};
CacheIndexer.Add(cachedImage);
}
}
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.CachedPath);
isUpdated = true;
}
}
return isUpdated;
}
#region Methods
#region Public
/// <summary>
/// Trims a cached folder ensuring that it does not exceed the maximum file count.
/// Adds the image to the cache in an asynchronous manner.
/// </summary>
/// <param name="path">
/// The path to the folder.
/// <param name="stream">
/// The stream containing the image data.
/// </param>
public static void TrimCachedFolders(string path)
/// <param name="contentType">
/// The content type of the image.
/// </param>
/// <returns>
/// The <see cref="Task"/> representing an asynchronous operation.
/// </returns>
public override async Task AddImageToCacheAsync(Stream stream, string contentType)
{
// ReSharper disable once AssignNullToNotNullAttribute
DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.CachedPath));
if (!directoryInfo.Exists)
{
directoryInfo.Create();
}
using (FileStream fileStream = File.Create(this.CachedPath))
{
await stream.CopyToAsync(fileStream);
}
}
/// <summary>
/// Trims the cache of any expired items in an asynchronous manner.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> representing an asynchronous operation.
/// </returns>
public override async Task TrimCacheAsync()
{
string directory = Path.GetDirectoryName(path);
string directory = Path.GetDirectoryName(this.CachedPath);
if (directory != null)
{
@ -148,7 +202,7 @@ namespace ImageProcessor.Web.Caching
if (parentDirectoryInfo != null)
{
// UNC folders can throw exceptions if the file doesn't exist.
foreach (DirectoryInfo enumerateDirectory in parentDirectoryInfo.SafeEnumerateDirectories())
foreach (DirectoryInfo enumerateDirectory in await parentDirectoryInfo.SafeEnumerateDirectoriesAsync())
{
IEnumerable<FileInfo> files = enumerateDirectory.EnumerateFiles().OrderBy(f => f.CreationTimeUtc);
int count = files.Count();
@ -160,7 +214,7 @@ namespace ImageProcessor.Web.Caching
// 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 (!IsExpired(fileInfo.CreationTimeUtc) && count <= MaxFilesCount - 1)
if (!this.IsExpired(fileInfo.CreationTimeUtc) && count <= MaxFilesCount - 1)
{
break;
}
@ -182,129 +236,15 @@ namespace ImageProcessor.Web.Caching
}
/// <summary>
/// Adds an image to the cache.
/// </summary>
/// <param name="cachedPath">
/// The path to the cached image.
/// </param>
public void AddImageToCache(string cachedPath)
{
string key = Path.GetFileNameWithoutExtension(cachedPath);
CachedImage cachedImage = new CachedImage
{
Key = key,
Path = cachedPath,
CreationTimeUtc = DateTime.UtcNow
};
CacheIndexer.Add(cachedImage);
}
/// <summary>
/// Returns a value indicating whether the original file is new or has been updated.
/// Rewrites the path to point to the cached image.
/// </summary>
/// <param name="cachedPath">
/// The path to the cached image.
/// <param name="context">
/// The <see cref="HttpContext"/> encapsulating all information about the request.
/// </param>
/// <returns>
/// True if The original file is new or has been updated; otherwise, false.
/// </returns>
public bool IsNewOrUpdatedFile(string cachedPath)
public override void RewritePath(HttpContext context)
{
bool isUpdated = false;
CachedImage cachedImage = CacheIndexer.GetValue(cachedPath);
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 (IsExpired(cachedImage.CreationTimeUtc))
{
CacheIndexer.Remove(cachedPath);
isUpdated = true;
}
}
return isUpdated;
}
#endregion
#region Private
/// <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 static bool IsExpired(DateTime creationDate)
{
return creationDate.AddDays(MaxFileCachedDuration) < DateTime.UtcNow.AddDays(-MaxFileCachedDuration);
}
/// <summary>
/// Gets the full transformed cached paths for the image.
/// The images are stored in paths that are based upon the SHA1 of their full request path
/// taking the individual characters of the hash to determine their location.
/// This allows us to store millions of images.
/// </summary>
private void GetCachePaths()
{
string streamHash = string.Empty;
if (AbsoluteCachePath != null)
{
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();
// Collision rate of about 1 in 10000 for the folder structure.
string pathFromKey = string.Join("\\", encryptedName.ToCharArray().Take(6));
string virtualPathFromKey = pathFromKey.Replace(@"\", "/");
string cachedFileName = string.Format(
"{0}.{1}",
encryptedName,
!string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension.Replace(".", string.Empty) : "jpg");
this.physicalCachedPath = Path.Combine(AbsoluteCachePath, pathFromKey, cachedFileName);
this.virtualCachedPath = Path.Combine(VirtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/");
}
// The cached file is valid so just rewrite the path.
context.RewritePath(this.virtualCachedFilePath, false);
}
#endregion
#endregion
}
}

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

@ -0,0 +1,84 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="IImageCache.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Defines properties and methods for allowing caching of images to different sources.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Caching
{
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Web;
/// <summary>
/// Defines properties and methods for allowing caching of images to different sources.
/// </summary>
public interface IImageCache
{
/// <summary>
/// Gets or sets any additional settings required by the cache.
/// </summary>
Dictionary<string, string> Settings { get; set; }
/// <summary>
/// Gets the path to the cached image.
/// </summary>
string CachedPath { get; }
/// <summary>
/// Gets the maximum number of days to store the image.
/// </summary>
int MaxDays { get; }
/// <summary>
/// Gets a value indicating whether the image is new or updated in an asynchronous manner.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> returning the value.
/// </returns>
Task<bool> IsNewOrUpdatedAsync();
/// <summary>
/// Adds the image to the cache in an asynchronous manner.
/// </summary>
/// <param name="stream">
/// The stream containing the image data.
/// </param>
/// <param name="contentType">
/// The content type of the image.
/// </param>
/// <returns>
/// The <see cref="Task"/> representing an asynchronous operation.
/// </returns>
Task AddImageToCacheAsync(Stream stream, string contentType);
/// <summary>
/// Trims the cache of any expired items in an asynchronous manner.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> representing an asynchronous operation.
/// </returns>
Task TrimCacheAsync();
/// <summary>
/// Gets a string identifying the cached file name.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> returning the value.
/// </returns>
Task<string> CreateCachedFileName();
/// <summary>
/// Rewrites the path to point to the cached image.
/// </summary>
/// <param name="context">
/// The <see cref="HttpContext"/> encapsulating all information about the request.
/// </param>
void RewritePath(HttpContext context);
}
}

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

@ -0,0 +1,182 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ImageCacheBase.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// The image cache base provides methods for implementing the <see cref="IImageCache" /> interface.
// It is recommended that any implementations inherit from this class.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Caching
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Web.Configuration;
using ImageProcessor.Web.Extensions;
using ImageProcessor.Web.Helpers;
/// <summary>
/// The image cache base provides methods for implementing the <see cref="IImageCache"/> interface.
/// It is recommended that any implementations inherit from this class.
/// </summary>
public abstract class ImageCacheBase : IImageCache
{
/// <summary>
/// The request path for the image.
/// </summary>
protected readonly string RequestPath;
/// <summary>
/// The full path for the image.
/// </summary>
protected readonly string FullPath;
/// <summary>
/// The querystring containing processing instructions.
/// </summary>
protected 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;
this.Settings = ImageProcessorConfiguration.Instance.ImageCacheSettings;
}
/// <summary>
/// Gets or sets any additional settings required by the cache.
/// </summary>
public Dictionary<string, string> Settings { get; set; }
/// <summary>
/// Gets or sets the path to the cached image.
/// </summary>
public string CachedPath { get; set; }
/// <summary>
/// Gets the maximum number of days to store the image.
/// </summary>
public abstract int MaxDays { get; }
/// <summary>
/// Gets a value indicating whether the image is new or updated in an asynchronous manner.
/// </summary>
/// <returns>
/// The <see cref="Task"/>.
/// </returns>
public abstract Task<bool> IsNewOrUpdatedAsync();
/// <summary>
/// Adds the image to the cache in an asynchronous manner.
/// </summary>
/// <param name="stream">
/// The stream containing the image data.
/// </param>
/// <param name="contentType">
/// The content type of the image.
/// </param>
/// <returns>
/// The <see cref="Task"/> representing an asynchronous operation.
/// </returns>
public abstract Task AddImageToCacheAsync(Stream stream, string contentType);
/// <summary>
/// Trims the cache of any expired items in an asynchronous manner.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> representing an asynchronous operation.
/// </returns>
public abstract Task TrimCacheAsync();
/// <summary>
/// Gets a string identifying the cached file name.
/// </summary>
/// <returns>
/// The asynchronous <see cref="Task"/> returning the value.
/// </returns>
public virtual 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");
return Task.FromResult(cachedFileName);
}
/// <summary>
/// Rewrites the path to point to the cached image.
/// </summary>
/// <param name="context">
/// The <see cref="HttpContext"/> encapsulating all information about the request.
/// </param>
public abstract void RewritePath(HttpContext context);
/// <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>
protected virtual bool IsExpired(DateTime creationDate)
{
return creationDate.AddDays(this.MaxDays) < DateTime.UtcNow.AddDays(-this.MaxDays);
}
}
}

157
src/ImageProcessor.Web/Configuration/ImageCacheSection.cs

@ -14,7 +14,6 @@ namespace ImageProcessor.Web.Configuration
using System.IO;
using System.Xml;
using ImageProcessor.Web.Extensions;
using ImageProcessor.Web.Helpers;
/// <summary>
@ -23,43 +22,34 @@ namespace ImageProcessor.Web.Configuration
public sealed class ImageCacheSection : ConfigurationSection
{
/// <summary>
/// Gets or sets the virtual path of the cache folder.
/// Gets or sets the name of the current cache provider.
/// </summary>
/// <value>The name of the cache folder.</value>
[ConfigurationProperty("virtualPath", DefaultValue = "~/app_data/cache", IsRequired = true)]
[StringValidator(MinLength = 3, MaxLength = 256)]
public string VirtualPath
[ConfigurationProperty("currentCache", DefaultValue = "DiskCache", IsRequired = true)]
public string CurrentCache
{
get
{
string virtualPath = (string)this["virtualPath"];
return virtualPath.IsValidVirtualPathName() ? virtualPath : "~/app_data/cache";
return (string)this["currentCache"];
}
set
{
this["virtualPath"] = value;
this["currentCache"] = value;
}
}
/// <summary>
/// Gets or sets the maximum number of days to store an image in the cache.
/// Gets the <see cref="CacheElementCollection"/>
/// </summary>
/// <value>The maximum number of days to store an image in the cache.</value>
/// <remarks>Defaults to 28 if not set.</remarks>
[ConfigurationProperty("maxDays", DefaultValue = "365", IsRequired = false)]
[IntegerValidator(ExcludeRange = false, MinValue = 0)]
public int MaxDays
/// <value>The <see cref="CacheElementCollection"/></value>
[ConfigurationProperty("caches", IsRequired = true)]
public CacheElementCollection ImageCaches
{
get
{
return (int)this["maxDays"];
}
set
{
this["maxDays"] = value;
object o = this["caches"];
return o as CacheElementCollection;
}
}
@ -69,7 +59,7 @@ namespace ImageProcessor.Web.Configuration
/// <returns>The cache configuration section from the current application configuration.</returns>
public static ImageCacheSection GetConfiguration()
{
ImageCacheSection imageCacheSection = ConfigurationManager.GetSection("imageProcessor/cache") as ImageCacheSection;
ImageCacheSection imageCacheSection = ConfigurationManager.GetSection("imageProcessor/caching") as ImageCacheSection;
if (imageCacheSection != null)
{
@ -83,5 +73,128 @@ namespace ImageProcessor.Web.Configuration
return imageCacheSection;
}
/// <summary>
/// Represents a CacheElement configuration element within the configuration.
/// </summary>
public class CacheElement : ConfigurationElement
{
/// <summary>
/// Gets or sets the name of the cache.
/// </summary>
/// <value>The name of the service.</value>
[ConfigurationProperty("name", DefaultValue = "", IsRequired = true)]
public string Name
{
get { return (string)this["name"]; }
set { this["name"] = value; }
}
/// <summary>
/// Gets or sets the type of the cache.
/// </summary>
/// <value>The full Type definition of the service</value>
[ConfigurationProperty("type", DefaultValue = "", IsRequired = true)]
public string Type
{
get { return (string)this["type"]; }
set { this["type"] = value; }
}
/// <summary>
/// Gets the <see cref="SettingElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="SettingElementCollection"/>.
/// </value>
[ConfigurationProperty("settings", IsRequired = false)]
public SettingElementCollection Settings
{
get
{
return this["settings"] as SettingElementCollection;
}
}
}
/// <summary>
/// Represents a collection of <see cref="CacheElement"/> elements within the configuration.
/// </summary>
public class CacheElementCollection : ConfigurationElementCollection
{
/// <summary>
/// Gets the type of the <see cref="ConfigurationElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="ConfigurationElementCollectionType"/> of this collection.
/// </value>
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
/// <summary>
/// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class.
/// </summary>
/// <value>
/// The name of the collection; otherwise, an empty string. The default is an empty string.
/// </value>
protected override string ElementName
{
get { return "cache"; }
}
/// <summary>
/// Gets or sets the <see cref="CacheElement"/>
/// at the specified index within the collection.
/// </summary>
/// <param name="index">The index at which to get the specified object.</param>
/// <returns>
/// The <see cref="CacheElement"/>
/// at the specified index within the collection.
/// </returns>
public CacheElement this[int index]
{
get
{
return (CacheElement)BaseGet(index);
}
set
{
if (this.BaseGet(index) != null)
{
this.BaseRemoveAt(index);
}
this.BaseAdd(index, value);
}
}
/// <summary>
/// When overridden in a derived class, creates a new <see cref="ConfigurationElement"/>.
/// </summary>
/// <returns>
/// A new <see cref="ConfigurationElement"/>.
/// </returns>
protected override ConfigurationElement CreateNewElement()
{
return new CacheElement();
}
/// <summary>
/// Gets the element key for a specified configuration element when overridden in a derived class.
/// </summary>
/// <returns>
/// An <see cref="T:System.Object"/> that acts as the key for the specified <see cref="ConfigurationElement"/>.
/// </returns>
/// <param name="element">The <see cref="ConfigurationElement"/> to return the key for. </param>
protected override object GetElementKey(ConfigurationElement element)
{
return ((CacheElement)element).Name;
}
}
}
}

156
src/ImageProcessor.Web/Configuration/ImageProcessingSection.cs

@ -131,10 +131,10 @@ namespace ImageProcessor.Web.Configuration
public class PresetElementCollection : ConfigurationElementCollection
{
/// <summary>
/// Gets the type of the <see cref="T:System.Configuration.ConfigurationElementCollection"/>.
/// Gets the type of the <see cref="ConfigurationElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="T:System.Configuration.ConfigurationElementCollectionType"/> of this collection.
/// The <see cref="ConfigurationElementCollectionType"/> of this collection.
/// </value>
public override ConfigurationElementCollectionType CollectionType
{
@ -194,7 +194,7 @@ namespace ImageProcessor.Web.Configuration
/// Gets the element key for a specified PluginElement configuration element.
/// </summary>
/// <param name="element">
/// The <see cref="T:System.Configuration.ConfigurationElement">ConfigurationElement</see>
/// The <see cref="ConfigurationElement">ConfigurationElement</see>
/// to return the key for.
/// </param>
/// <returns>The element key for a specified PluginElement configuration element.</returns>
@ -255,10 +255,10 @@ namespace ImageProcessor.Web.Configuration
public class PluginElementCollection : ConfigurationElementCollection
{
/// <summary>
/// Gets the type of the <see cref="T:System.Configuration.ConfigurationElementCollection"/>.
/// Gets the type of the <see cref="ConfigurationElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="T:System.Configuration.ConfigurationElementCollectionType"/> of this collection.
/// The <see cref="ConfigurationElementCollectionType"/> of this collection.
/// </value>
public override ConfigurationElementCollectionType CollectionType
{
@ -318,7 +318,7 @@ namespace ImageProcessor.Web.Configuration
/// Gets the element key for a specified PluginElement configuration element.
/// </summary>
/// <param name="element">
/// The <see cref="T:System.Configuration.ConfigurationElement">ConfigurationElement</see>
/// The <see cref="ConfigurationElement">ConfigurationElement</see>
/// to return the key for.
/// </param>
/// <returns>The element key for a specified PluginElement configuration element.</returns>
@ -327,149 +327,5 @@ namespace ImageProcessor.Web.Configuration
return ((PluginElement)element).Name;
}
}
/// <summary>
/// Represents a SettingElement configuration element within the configuration.
/// </summary>
public class SettingElement : ConfigurationElement
{
/// <summary>
/// Gets or sets the key of the plugin setting.
/// </summary>
/// <value>The key of the plugin setting.</value>
[ConfigurationProperty("key", IsRequired = true, IsKey = true)]
public string Key
{
get
{
return this["key"] as string;
}
set
{
this["key"] = value;
}
}
/// <summary>
/// Gets or sets the value of the plugin setting.
/// </summary>
/// <value>The value of the plugin setting.</value>
[ConfigurationProperty("value", IsRequired = true)]
public string Value
{
get
{
return (string)this["value"];
}
set
{
this["value"] = value;
}
}
}
/// <summary>
/// Represents a SettingElementCollection collection configuration element within the configuration.
/// </summary>
public class SettingElementCollection : ConfigurationElementCollection
{
/// <summary>
/// Gets the type of the <see cref="T:System.Configuration.ConfigurationElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="T:System.Configuration.ConfigurationElementCollectionType"/> of this collection.
/// </value>
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
/// <summary>
/// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class.
/// </summary>
/// <value>
/// The name of the collection; otherwise, an empty string. The default is an empty string.
/// </value>
protected override string ElementName
{
get { return "setting"; }
}
/// <summary>
/// Gets or sets the <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElement"/>
/// at the specified index within the collection.
/// </summary>
/// <param name="index">The index at which to get the specified object.</param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageProcessingSection.SettingElement"/>
/// at the specified index within the collection.
/// </returns>
public SettingElement this[int index]
{
get
{
return (SettingElement)BaseGet(index);
}
set
{
if (this.BaseGet(index) != null)
{
this.BaseRemoveAt(index);
}
this.BaseAdd(index, value);
}
}
/// <summary>
/// Returns the setting element with the specified key.
/// </summary>
/// <param name="key">the key representing the element</param>
/// <returns>the setting element</returns>
public new SettingElement this[string key]
{
get { return (SettingElement)BaseGet(key); }
}
/// <summary>
/// Returns a value indicating whether the settings collection contains the
/// given object.
/// </summary>
/// <param name="key">The key to identify the setting.</param>
/// <returns>True if the collection contains the key; otherwise false.</returns>
public bool ContainsKey(string key)
{
object[] keys = BaseGetAllKeys();
return keys.Any(obj => (string)obj == key);
}
/// <summary>
/// Gets the element key for a specified PluginElement configuration element.
/// </summary>
/// <param name="element">
/// The <see cref="T:System.Configuration.ConfigurationElement">ConfigurationElement</see>
/// to return the key for.
/// </param>
/// <returns>The element key for a specified PluginElement configuration element.</returns>
protected override object GetElementKey(ConfigurationElement element)
{
return ((SettingElement)element).Key;
}
/// <summary>
/// Creates a new SettingElement configuration element.
/// </summary>
/// <returns>
/// A new SettingElement configuration element.
/// </returns>
protected override ConfigurationElement CreateNewElement()
{
return new SettingElement();
}
}
}
}

72
src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs

@ -19,6 +19,7 @@ namespace ImageProcessor.Web.Configuration
using ImageProcessor.Common.Extensions;
using ImageProcessor.Processors;
using ImageProcessor.Web.Caching;
using ImageProcessor.Web.Processors;
using ImageProcessor.Web.Services;
@ -67,6 +68,7 @@ namespace ImageProcessor.Web.Configuration
{
this.LoadGraphicsProcessors();
this.LoadImageServices();
this.LoadImageCache();
}
#endregion
@ -93,41 +95,26 @@ namespace ImageProcessor.Web.Configuration
public IList<IImageService> ImageServices { get; private set; }
/// <summary>
/// Gets a value indicating whether to preserve exif meta data.
/// Gets the current image cache.
/// </summary>
public bool PreserveExifMetaData
{
get
{
return GetImageProcessingSection().PreserveExifMetaData;
}
}
public Type ImageCache { get; private set; }
#region Caching
/// <summary>
/// Gets the maximum number of days to store images in the cache.
/// Gets the image cache settings.
/// </summary>
public int MaxCacheDays
{
get
{
return GetImageCacheSection().MaxDays;
}
}
public Dictionary<string, string> ImageCacheSettings { get; private set; }
/// <summary>
/// Gets or the virtual path of the cache folder.
/// Gets a value indicating whether to preserve exif meta data.
/// </summary>
/// <value>The virtual path of the cache folder.</value>
public string VirtualCachePath
public bool PreserveExifMetaData
{
get
{
return GetImageCacheSection().VirtualPath;
return GetImageProcessingSection().PreserveExifMetaData;
}
}
#endregion
#endregion
#region Methods
/// <summary>
@ -271,7 +258,7 @@ namespace ImageProcessor.Web.Configuration
if (pluginElement != null)
{
settings = pluginElement.Settings
.Cast<ImageProcessingSection.SettingElement>()
.Cast<SettingElement>()
.ToDictionary(setting => setting.Key, setting => setting.Value);
}
else
@ -367,13 +354,13 @@ namespace ImageProcessor.Web.Configuration
}
/// <summary>
/// Returns the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElementCollection"/> for the given plugin.
/// Returns the <see cref="SettingElementCollection"/> for the given plugin.
/// </summary>
/// <param name="name">
/// The name of the plugin to get the settings for.
/// </param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElementCollection"/> for the given plugin.
/// The <see cref="SettingElementCollection"/> for the given plugin.
/// </returns>
private Dictionary<string, string> GetServiceSettings(string name)
{
@ -387,7 +374,7 @@ namespace ImageProcessor.Web.Configuration
if (serviceElement != null)
{
settings = serviceElement.Settings
.Cast<ImageSecuritySection.SettingElement>()
.Cast<SettingElement>()
.ToDictionary(setting => setting.Key, setting => setting.Value);
}
else
@ -424,6 +411,39 @@ namespace ImageProcessor.Web.Configuration
return whitelist;
}
#endregion
#region ImageCaches
/// <summary>
/// Gets the currently assigned <see cref="IImageCache"/>.
/// </summary>
private void LoadImageCache()
{
if (this.ImageCache == null)
{
string curentCache = GetImageCacheSection().CurrentCache;
ImageCacheSection.CacheElementCollection caches = imageCacheSection.ImageCaches;
foreach (ImageCacheSection.CacheElement cache in caches)
{
if (cache.Name == curentCache)
{
Type type = Type.GetType(cache.Type);
if (type == null)
{
throw new TypeLoadException("Couldn't load IImageCache: " + cache.Type);
}
this.ImageCache = type;
this.ImageCacheSettings = cache.Settings
.Cast<SettingElement>()
.ToDictionary(setting => setting.Key, setting => setting.Value);
break;
}
}
}
}
#endregion
#endregion
}
}

171
src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs

@ -13,7 +13,6 @@ namespace ImageProcessor.Web.Configuration
using System;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Xml;
using ImageProcessor.Web.Helpers;
@ -23,11 +22,10 @@ namespace ImageProcessor.Web.Configuration
/// </summary>
public sealed class ImageSecuritySection : ConfigurationSection
{
#region Properties
/// <summary>
/// Gets the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.ServiceElementCollection"/>
/// Gets the <see cref="ServiceElementCollection"/>
/// </summary>
/// <value>The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.ServiceElementCollection"/></value>
/// <value>The <see cref="ServiceElementCollection"/></value>
[ConfigurationProperty("services", IsRequired = true)]
public ServiceElementCollection ImageServices
{
@ -42,9 +40,7 @@ namespace ImageProcessor.Web.Configuration
/// Gets or sets a value indicating whether to auto load services.
/// </summary>
public bool AutoLoadServices { get; set; }
#endregion
#region Methods
/// <summary>
/// Retrieves the security configuration section from the current application configuration.
/// </summary>
@ -66,7 +62,6 @@ namespace ImageProcessor.Web.Configuration
imageSecuritySection.AutoLoadServices = true;
return imageSecuritySection;
}
#endregion
/// <summary>
/// Represents a ServiceElement configuration element within the configuration.
@ -110,10 +105,10 @@ namespace ImageProcessor.Web.Configuration
}
/// <summary>
/// Gets the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElementCollection"/>.
/// Gets the <see cref="SettingElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElementCollection"/>.
/// The <see cref="SettingElementCollection"/>.
/// </value>
[ConfigurationProperty("settings", IsRequired = false)]
public SettingElementCollection Settings
@ -146,10 +141,10 @@ namespace ImageProcessor.Web.Configuration
public class ServiceElementCollection : ConfigurationElementCollection
{
/// <summary>
/// Gets the type of the <see cref="T:System.Configuration.ConfigurationElementCollection"/>.
/// Gets the type of the <see cref="ConfigurationElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="T:System.Configuration.ConfigurationElementCollectionType"/> of this collection.
/// The <see cref="ConfigurationElementCollectionType"/> of this collection.
/// </value>
public override ConfigurationElementCollectionType CollectionType
{
@ -195,10 +190,10 @@ namespace ImageProcessor.Web.Configuration
}
/// <summary>
/// When overridden in a derived class, creates a new <see cref="T:System.Configuration.ConfigurationElement"/>.
/// When overridden in a derived class, creates a new <see cref="ConfigurationElement"/>.
/// </summary>
/// <returns>
/// A new <see cref="T:System.Configuration.ConfigurationElement"/>.
/// A new <see cref="ConfigurationElement"/>.
/// </returns>
protected override ConfigurationElement CreateNewElement()
{
@ -209,159 +204,15 @@ namespace ImageProcessor.Web.Configuration
/// Gets the element key for a specified configuration element when overridden in a derived class.
/// </summary>
/// <returns>
/// An <see cref="T:System.Object"/> that acts as the key for the specified <see cref="T:System.Configuration.ConfigurationElement"/>.
/// An <see cref="T:System.Object"/> that acts as the key for the specified <see cref="ConfigurationElement"/>.
/// </returns>
/// <param name="element">The <see cref="T:System.Configuration.ConfigurationElement"/> to return the key for. </param>
/// <param name="element">The <see cref="ConfigurationElement"/> to return the key for. </param>
protected override object GetElementKey(ConfigurationElement element)
{
return ((ServiceElement)element).Name;
}
}
/// <summary>
/// Represents a SettingElement configuration element within the configuration.
/// </summary>
public class SettingElement : ConfigurationElement
{
/// <summary>
/// Gets or sets the key of the plugin setting.
/// </summary>
/// <value>The key of the plugin setting.</value>
[ConfigurationProperty("key", IsRequired = true, IsKey = true)]
public string Key
{
get
{
return this["key"] as string;
}
set
{
this["key"] = value;
}
}
/// <summary>
/// Gets or sets the value of the plugin setting.
/// </summary>
/// <value>The value of the plugin setting.</value>
[ConfigurationProperty("value", IsRequired = true)]
public string Value
{
get
{
return (string)this["value"];
}
set
{
this["value"] = value;
}
}
}
/// <summary>
/// Represents a SettingElementCollection collection configuration element within the configuration.
/// </summary>
public class SettingElementCollection : ConfigurationElementCollection
{
/// <summary>
/// Gets the type of the <see cref="T:System.Configuration.ConfigurationElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="T:System.Configuration.ConfigurationElementCollectionType"/> of this collection.
/// </value>
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
/// <summary>
/// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class.
/// </summary>
/// <value>
/// The name of the collection; otherwise, an empty string. The default is an empty string.
/// </value>
protected override string ElementName
{
get { return "setting"; }
}
/// <summary>
/// Gets or sets the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElement"/>
/// at the specified index within the collection.
/// </summary>
/// <param name="index">The index at which to get the specified object.</param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElement"/>
/// at the specified index within the collection.
/// </returns>
public SettingElement this[int index]
{
get
{
return (SettingElement)BaseGet(index);
}
set
{
if (this.BaseGet(index) != null)
{
this.BaseRemoveAt(index);
}
this.BaseAdd(index, value);
}
}
/// <summary>
/// Returns the setting element with the specified key.
/// </summary>
/// <param name="key">the key representing the element</param>
/// <returns>the setting element</returns>
public new SettingElement this[string key]
{
get { return (SettingElement)BaseGet(key); }
}
/// <summary>
/// Returns a value indicating whether the settings collection contains the
/// given object.
/// </summary>
/// <param name="key">The key to identify the setting.</param>
/// <returns>True if the collection contains the key; otherwise false.</returns>
public bool ContainsKey(string key)
{
object[] keys = BaseGetAllKeys();
return keys.Any(obj => (string)obj == key);
}
/// <summary>
/// Gets the element key for a specified PluginElement configuration element.
/// </summary>
/// <param name="element">
/// The <see cref="T:System.Configuration.ConfigurationElement">ConfigurationElement</see>
/// to return the key for.
/// </param>
/// <returns>The element key for a specified PluginElement configuration element.</returns>
protected override object GetElementKey(ConfigurationElement element)
{
return ((SettingElement)element).Key;
}
/// <summary>
/// Creates a new SettingElement configuration element.
/// </summary>
/// <returns>
/// A new SettingElement configuration element.
/// </returns>
protected override ConfigurationElement CreateNewElement()
{
return new SettingElement();
}
}
/// <summary>
/// Represents a whitelist collection configuration element within the configuration.
/// </summary>
@ -404,7 +255,7 @@ namespace ImageProcessor.Web.Configuration
/// <summary>
/// Gets the element key for a specified whitelist configuration element.
/// </summary>
/// <param name="element">The <see cref="T:System.Configuration.ConfigurationElement">ConfigurationElement</see> to return the key for.</param>
/// <param name="element">The <see cref="ConfigurationElement">ConfigurationElement</see> to return the key for.</param>
/// <returns>The element key for a specified whitelist configuration element.</returns>
protected override object GetElementKey(ConfigurationElement element)
{

11
src/ImageProcessor.Web/Configuration/Resources/cache.config

@ -1 +1,10 @@
<cache virtualPath="~/app_data/cache" maxDays="365"/>
<caching currentCache="DiskCache">
<caches>
<cache name="DiskCache" type="ImageProcessor.Web.Caching.DiskCache, ImageProcessor.Web">
<settings>
<setting key="MaxAge" value="365"/>
<setting key="VirtualCachePath" value="~/app_data/cache"/>
</settings>
</cache>
</caches>
</caching>

1
src/ImageProcessor.Web/Configuration/Resources/security.config

@ -5,6 +5,7 @@
<settings>
<setting key="MaxBytes" value="4194304"/>
<setting key="Timeout" value="3000"/>
<setting key="Protocol" value="http"/>
</settings>
<whitelist>
</whitelist>

56
src/ImageProcessor.Web/Configuration/Shared/SettingElement.cs

@ -0,0 +1,56 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SettingElement.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Represents a SettingElement configuration element within the configuration.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Configuration
{
using System.Configuration;
/// <summary>
/// Represents a SettingElement configuration element within the configuration.
/// </summary>
public class SettingElement : ConfigurationElement
{
/// <summary>
/// Gets or sets the key of the plugin setting.
/// </summary>
/// <value>The key of the plugin setting.</value>
[ConfigurationProperty("key", IsRequired = true, IsKey = true)]
public string Key
{
get
{
return this["key"] as string;
}
set
{
this["key"] = value;
}
}
/// <summary>
/// Gets or sets the value of the plugin setting.
/// </summary>
/// <value>The value of the plugin setting.</value>
[ConfigurationProperty("value", IsRequired = true)]
public string Value
{
get
{
return (string)this["value"];
}
set
{
this["value"] = value;
}
}
}
}

117
src/ImageProcessor.Web/Configuration/Shared/SettingElementCollection.cs

@ -0,0 +1,117 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SettingElementCollection.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Represents a SettingElementCollection collection configuration element within the configuration.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Configuration
{
using System.Configuration;
using System.Linq;
/// <summary>
/// Represents a SettingElementCollection collection configuration element within the configuration.
/// </summary>
public class SettingElementCollection : ConfigurationElementCollection
{
/// <summary>
/// Gets the type of the <see cref="ConfigurationElementCollection"/>.
/// </summary>
/// <value>
/// The <see cref="ConfigurationElementCollectionType"/> of this collection.
/// </value>
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
/// <summary>
/// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class.
/// </summary>
/// <value>
/// The name of the collection; otherwise, an empty string. The default is an empty string.
/// </value>
protected override string ElementName
{
get { return "setting"; }
}
/// <summary>
/// Gets or sets the <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElement"/>
/// at the specified index within the collection.
/// </summary>
/// <param name="index">The index at which to get the specified object.</param>
/// <returns>
/// The <see cref="T:ImageProcessor.Web.Config.ImageSecuritySection.SettingElement"/>
/// at the specified index within the collection.
/// </returns>
public SettingElement this[int index]
{
get
{
return (SettingElement)BaseGet(index);
}
set
{
if (this.BaseGet(index) != null)
{
this.BaseRemoveAt(index);
}
this.BaseAdd(index, value);
}
}
/// <summary>
/// Returns the setting element with the specified key.
/// </summary>
/// <param name="key">the key representing the element</param>
/// <returns>the setting element</returns>
public new SettingElement this[string key]
{
get { return (SettingElement)BaseGet(key); }
}
/// <summary>
/// Returns a value indicating whether the settings collection contains the
/// given object.
/// </summary>
/// <param name="key">The key to identify the setting.</param>
/// <returns>True if the collection contains the key; otherwise false.</returns>
public bool ContainsKey(string key)
{
object[] keys = BaseGetAllKeys();
return keys.Any(obj => (string)obj == key);
}
/// <summary>
/// Gets the element key for a specified PluginElement configuration element.
/// </summary>
/// <param name="element">
/// The <see cref="ConfigurationElement">ConfigurationElement</see>
/// to return the key for.
/// </param>
/// <returns>The element key for a specified PluginElement configuration element.</returns>
protected override object GetElementKey(ConfigurationElement element)
{
return ((SettingElement)element).Key;
}
/// <summary>
/// Creates a new SettingElement configuration element.
/// </summary>
/// <returns>
/// A new SettingElement configuration element.
/// </returns>
protected override ConfigurationElement CreateNewElement()
{
return new SettingElement();
}
}
}

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;

180
src/ImageProcessor.Web/Extensions/TypeInitializationExtensions.cs

@ -0,0 +1,180 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="TypeInitializationExtensions.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Extensions methods for <see cref="T:System.Type" /> for creating instances of types faster than
// using reflection. Modified from the original class at.
// <see href="http://geekswithblogs.net/mrsteve/archive/2012/02/19/a-fast-c-sharp-extension-method-using-expression-trees-create-instance-from-type-again.aspx" />
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.Extensions
{
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
/// <summary>
/// Extensions methods for <see cref="T:System.Type"/> for creating instances of types faster than
/// using reflection. Modified from the original class at.
/// <see href="http://geekswithblogs.net/mrsteve/archive/2012/02/19/a-fast-c-sharp-extension-method-using-expression-trees-create-instance-from-type-again.aspx"/>
/// </summary>
internal static class TypeInitializationExtensions
{
/// <summary>
/// Returns an instance of the <paramref name="type"/> on which the method is invoked.
/// </summary>
/// <param name="type">The type on which the method was invoked.</param>
/// <returns>An instance of the <paramref name="type"/>.</returns>
public static object GetInstance(this Type type)
{
// This is about as quick as it gets.
return Activator.CreateInstance(type);
}
/// <summary>
/// Returns an instance of the <paramref name="type"/> on which the method is invoked.
/// </summary>
/// <typeparam name="TArg">The type of the argument to pass to the constructor.</typeparam>
/// <param name="type">The type on which the method was invoked.</param>
/// <param name="argument">The argument to pass to the constructor.</param>
/// <returns>An instance of the given <paramref name="type"/>.</returns>
public static object GetInstance<TArg>(this Type type, TArg argument)
{
return GetInstance<TArg, TypeToIgnore>(type, argument, null);
}
/// <summary>
/// Returns an instance of the <paramref name="type"/> on which the method is invoked.
/// </summary>
/// <typeparam name="TArg1">The type of the first argument to pass to the constructor.</typeparam>
/// <typeparam name="TArg2">The type of the second argument to pass to the constructor.</typeparam>
/// <param name="type">The type on which the method was invoked.</param>
/// <param name="argument1">The first argument to pass to the constructor.</param>
/// <param name="argument2">The second argument to pass to the constructor.</param>
/// <returns>An instance of the given <paramref name="type"/>.</returns>
public static object GetInstance<TArg1, TArg2>(this Type type, TArg1 argument1, TArg2 argument2)
{
return GetInstance<TArg1, TArg2, TypeToIgnore>(type, argument1, argument2, null);
}
/// <summary>
/// Returns an instance of the <paramref name="type"/> on which the method is invoked.
/// </summary>
/// <typeparam name="TArg1">The type of the first argument to pass to the constructor.</typeparam>
/// <typeparam name="TArg2">The type of the second argument to pass to the constructor.</typeparam>
/// <typeparam name="TArg3">The type of the third argument to pass to the constructor.</typeparam>
/// <param name="type">The type on which the method was invoked.</param>
/// <param name="argument1">The first argument to pass to the constructor.</param>
/// <param name="argument2">The second argument to pass to the constructor.</param>
/// <param name="argument3">The third argument to pass to the constructor.</param>
/// <returns>An instance of the given <paramref name="type"/>.</returns>
public static object GetInstance<TArg1, TArg2, TArg3>(
this Type type,
TArg1 argument1,
TArg2 argument2,
TArg3 argument3)
{
return InstanceCreationFactory<TArg1, TArg2, TArg3>
.CreateInstanceOf(type, argument1, argument2, argument3);
}
/// <summary>
/// The instance creation factory for creating instances.
/// </summary>
/// <typeparam name="TArg1">The type of the first argument to pass to the constructor.</typeparam>
/// <typeparam name="TArg2">The type of the second argument to pass to the constructor.</typeparam>
/// <typeparam name="TArg3">The type of the third argument to pass to the constructor.</typeparam>
private static class InstanceCreationFactory<TArg1, TArg2, TArg3>
{
/// <summary>
/// This dictionary will hold a cache of object-creation functions, keyed by the Type to create:
/// </summary>
private static readonly ConcurrentDictionary<Type, Func<TArg1, TArg2, TArg3, object>> InstanceCreationMethods = new ConcurrentDictionary<Type, Func<TArg1, TArg2, TArg3, object>>();
/// <summary>
/// The create instance of.
/// </summary>
/// <param name="type">
/// The type.
/// </param>
/// <param name="arg1">The first argument to pass to the constructor.</param>
/// <param name="arg2">The second argument to pass to the constructor.</param>
/// <param name="arg3">The third argument to pass to the constructor.</param>
/// <returns>
/// The <see cref="object"/>.
/// </returns>
public static object CreateInstanceOf(Type type, TArg1 arg1, TArg2 arg2, TArg3 arg3)
{
CacheInstanceCreationMethodIfRequired(type);
return InstanceCreationMethods[type].Invoke(arg1, arg2, arg3);
}
/// <summary>
/// Caches the instance creation method.
/// </summary>
/// <param name="type">
/// The <see cref="Type"/> who's constructor to cache.
/// </param>
private static void CacheInstanceCreationMethodIfRequired(Type type)
{
// Bail out if we've already cached the instance creation method:
Func<TArg1, TArg2, TArg3, object> cached;
if (InstanceCreationMethods.TryGetValue(type, out cached))
{
return;
}
Type[] argumentTypes = { typeof(TArg1), typeof(TArg2), typeof(TArg3) };
// Get a collection of the constructor argument Types we've been given; ignore any
// arguments which are of the 'ignore this' Type:
Type[] constructorArgumentTypes = argumentTypes.Where(t => t != typeof(TypeToIgnore)).ToArray();
// Get the Constructor which matches the given argument Types:
ConstructorInfo constructor = type.GetConstructor(
BindingFlags.Instance | BindingFlags.Public,
null,
CallingConventions.HasThis,
constructorArgumentTypes,
new ParameterModifier[0]);
// Get a set of Expressions representing the parameters which will be passed to the Func:
ParameterExpression[] lamdaParameterExpressions =
{
Expression.Parameter(typeof(TArg1), "param1"),
Expression.Parameter(typeof(TArg2), "param2"),
Expression.Parameter(typeof(TArg3), "param3")
};
// Get a set of Expressions representing the parameters which will be passed to the constructor:
ParameterExpression[] constructorParameterExpressions =
lamdaParameterExpressions.Take(constructorArgumentTypes.Length).ToArray();
// Get an Expression representing the constructor call, passing in the constructor parameters:
NewExpression constructorCallExpression = Expression.New(constructor, constructorParameterExpressions.Cast<Expression>());
// Compile the Expression into a Func which takes three arguments and returns the constructed object:
Func<TArg1, TArg2, TArg3, object> constructorCallingLambda =
Expression.Lambda<Func<TArg1, TArg2, TArg3, object>>(
constructorCallExpression,
lamdaParameterExpressions).Compile();
InstanceCreationMethods.TryAdd(type, constructorCallingLambda);
}
}
/// <summary>
/// To allow for overloads with differing numbers of arguments, we flag arguments which should be
/// ignored by using this Type:
/// </summary>
private class TypeToIgnore
{
}
}
}

26
src/ImageProcessor.Web/Helpers/ImageHelpers.cs

@ -67,7 +67,7 @@ namespace ImageProcessor.Web.Helpers
// First check to see if the format processor is being used and test against that.
IWebGraphicsProcessor format = ImageProcessorConfiguration.Instance.GraphicsProcessors
.First(p => typeof(Format) == p.GetType());
.FirstOrDefault(p => typeof(Format) == p.GetType());
if (format != null)
{
@ -104,30 +104,6 @@ namespace ImageProcessor.Web.Helpers
return string.Empty;
}
/// <summary>
/// Get the correct mime-type for the given string input.
/// </summary>
/// <param name="path">
/// The path to the cached image.
/// </param>
/// <returns>
/// The <see cref="string"/> matching the correct mime-type.
/// </returns>
public static string GetMimeType(string path)
{
using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false))
{
ISupportedImageFormat format = FormatUtilities.GetFormat(file);
if (format != null)
{
return format.MimeType;
}
}
return string.Empty;
}
/// <summary>
/// Builds a regular expression from the <see cref="T:ImageProcessor.Imaging.Formats.ISupportedImageFormat"/> type, this allows extensibility.
/// </summary>

11
src/ImageProcessor.Web/Helpers/RemoteFile.cs

@ -173,16 +173,20 @@ namespace ImageProcessor.Web.Helpers
/// </returns>
internal async Task<WebResponse> GetWebResponseAsync()
{
WebResponse response;
WebResponse response = null;
try
{
response = await this.GetWebRequest().GetResponseAsync();
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.NameResolutionFailure)
if (response != null)
{
throw new HttpException(404, "No image exists at " + Uri);
HttpWebResponse errorResponse = (HttpWebResponse)ex.Response;
if (errorResponse.StatusCode == HttpStatusCode.NotFound)
{
throw new HttpException(404, "No image exists at " + this.Uri);
}
}
throw;
@ -217,7 +221,6 @@ namespace ImageProcessor.Web.Helpers
#endregion
#region Private
/// <summary>
/// Creates the WebRequest object used internally for this RemoteFile instance.
/// </summary>

19
src/ImageProcessor.Web/Helpers/TypePropertyHelpers.cs

@ -0,0 +1,19 @@
namespace ImageProcessor.Web.Extensions
{
using System;
using System.Linq.Expressions;
internal static class TypePropertyHelpers
{
public static string GetPropertyName<T>(Expression<Func<T>> expression)
{
MemberExpression member = expression.Body as MemberExpression;
if (member != null)
{
return member.Member.Name;
}
throw new ArgumentException("expression");
}
}
}

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

@ -3,34 +3,24 @@
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Processes any image requests within the web application.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Web.HttpModules
{
#region Using
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using System.Web.Security;
using ImageProcessor.Web.Caching;
using ImageProcessor.Web.Configuration;
using ImageProcessor.Web.Extensions;
using ImageProcessor.Web.Helpers;
using ImageProcessor.Web.Services;
#endregion
/// <summary>
/// Processes any image requests within the web application.
@ -38,6 +28,7 @@ namespace ImageProcessor.Web.HttpModules
public sealed class ImageProcessingModule : IHttpModule
{
#region Fields
/// <summary>
/// The key for storing the response type of the current image.
/// </summary>
@ -85,9 +76,15 @@ namespace ImageProcessor.Web.HttpModules
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The image cache.
/// </summary>
private IImageCache imageCache;
#endregion
#region Destructors
/// <summary>
/// Finalizes an instance of the <see cref="T:ImageProcessor.Web.HttpModules.ImageProcessingModule"/> class.
/// </summary>
@ -105,6 +102,7 @@ namespace ImageProcessor.Web.HttpModules
// readability and maintainability.
this.Dispose(false);
}
#endregion
/// <summary>
@ -130,6 +128,7 @@ namespace ImageProcessor.Web.HttpModules
public static event ProcessQuerystringEventHandler OnProcessQuerystring;
#region IHttpModule Members
/// <summary>
/// Initializes a module and prepares it to handle requests.
/// </summary>
@ -149,7 +148,7 @@ namespace ImageProcessor.Web.HttpModules
context.AddOnPostAuthorizeRequestAsync(postAuthorizeHelper.BeginEventHandler, postAuthorizeHelper.EndEventHandler);
EventHandlerTaskAsyncHelper postProcessHelper = new EventHandlerTaskAsyncHelper(this.PostProcessImage);
context.AddOnPostRequestHandlerExecuteAsync(postProcessHelper.BeginEventHandler, postProcessHelper.EndEventHandler);
context.AddOnEndRequestAsync(postProcessHelper.BeginEventHandler, postProcessHelper.EndEventHandler);
context.PreSendRequestHeaders += this.ContextPreSendRequestHeaders;
}
@ -172,7 +171,9 @@ namespace ImageProcessor.Web.HttpModules
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">If true, the object gets disposed.</param>
/// <param name="disposing">
/// If true, the object gets disposed.
/// </param>
private void Dispose(bool disposing)
{
if (this.isDisposed)
@ -190,6 +191,7 @@ namespace ImageProcessor.Web.HttpModules
// Note disposing is done.
this.isDisposed = true;
}
#endregion
/// <summary>
@ -222,35 +224,37 @@ 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];
if (cachedPathObject != null)
{
string cachedPath = cachedPathObject.ToString();
// Trim the cache.
DiskCache.TrimCachedFolders(cachedPath);
await this.imageCache.TrimCacheAsync();
string cachedPath = cachedPathObject.ToString();
// 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>
/// Occurs just before ASP.NET send HttpHeaders to the client.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">An <see cref="T:System.EventArgs">EventArgs</see> that contains the event data.</param>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <see cref="T:System.EventArgs">EventArgs</see> that contains the event data.
/// </param>
private void ContextPreSendRequestHeaders(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)sender).Context;
@ -258,17 +262,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
@ -302,6 +300,7 @@ namespace ImageProcessor.Web.HttpModules
string queryString = string.Empty;
string urlParameters = string.Empty;
// Legacy support. I'd like to remove this asap.
if (hasMultiParams)
{
// We need to split the querystring to get the actual values we want.
@ -340,7 +339,13 @@ namespace ImageProcessor.Web.HttpModules
}
else
{
requestPath = HttpUtility.UrlDecode(request.QueryString.ToString());
// Parse any protocol values from settings.
string protocol = currentService.Settings["Protocol"] != null
? currentService.Settings["Protocol"] + "://"
: string.Empty;
requestPath = protocol + request.Path.Replace(currentService.Prefix, string.Empty).TrimStart('/');
queryString = HttpUtility.UrlDecode(request.QueryString.ToString());
}
}
@ -361,6 +366,7 @@ namespace ImageProcessor.Web.HttpModules
string fullPath = string.Format("{0}{1}?{2}", requestPath, parts, queryString);
object resourcePath;
// More legacy support code.
if (hasMultiParams)
{
resourcePath = string.IsNullOrWhiteSpace(urlParameters)
@ -379,99 +385,71 @@ 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;
IPrincipal user = context.User ?? new GenericPrincipal(new GenericIdentity(string.Empty, string.Empty), new string[0]);
// 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);
this.imageCache = (IImageCache)ImageProcessorConfiguration.Instance
.ImageCache.GetInstance(requestPath, fullPath, queryString);
bool isAllowed = true;
// Is the file new or updated?
bool isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync();
string cachedPath = this.imageCache.CachedPath;
// Run the rewritten path past the authorization system again.
// We can then use the result as the default "AllowAccess" value
if (hasPermission && !context.SkipAuthorization)
// Only process if the file has been updated.
if (isNewOrUpdated)
{
isAllowed = UrlAuthorizationModule.CheckUrlAccessForPrincipal(virtualCachedPath, user, "GET");
}
if (isAllowed)
{
// 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))
using (MemoryStream inStream = new MemoryStream(imageBuffer))
{
// Process the Image
using (MemoryStream outStream = new MemoryStream())
{
// 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);
imageFactory.Load(inStream).AutoProcess(queryString).Save(outStream);
// Add to the cache.
cache.AddImageToCache(cachedPath);
// 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;
context.Items[CachedResponseFileDependency] = new List<string> { cachedPath };
await this.imageCache.AddImageToCacheAsync(outStream, imageFactory.CurrentImageFormat.MimeType);
}
}
}
}
// Image is from the cache so the mime-type will need to be set.
if (context.Items[CachedResponseTypeKey] == null)
{
string mimetype = ImageHelpers.GetMimeType(cachedPath);
// 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;
bool isFileCached = new Uri(cachedPath).IsFile;
if (!string.IsNullOrEmpty(mimetype))
{
context.Items[CachedResponseTypeKey] = mimetype;
if (isFileLocal)
{
if (isFileCached)
{
// 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] = Path.GetFileName(requestPath) == requestPath
? null
: new List<string> { requestPath };
}
}
else if (isFileCached)
{
context.Items[CachedResponseFileDependency] = new List<string> { cachedPath };
}
}
}
}
if (context.Items[CachedResponseFileDependency] == null)
{
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 };
}
}
// The cached file is valid so just rewrite the path.
this.imageCache.RewritePath(context);
// The cached file is valid so just rewrite the path.
context.RewritePath(virtualCachedPath, false);
}
else
// Redirect if not a locally store file.
if (!new Uri(cachedPath).IsFile)
{
throw new HttpException(403, "Access denied");
context.ApplicationInstance.CompleteRequest();
}
}
}
@ -494,28 +472,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.MaxDays;
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>
@ -586,11 +570,11 @@ namespace ImageProcessor.Web.HttpModules
IImageService imageService = null;
IList<IImageService> services = ImageProcessorConfiguration.Instance.ImageServices;
string path = request.Path;
string path = request.Path.TrimStart('/');
foreach (IImageService service in services)
{
string key = service.Prefix;
if (!string.IsNullOrWhiteSpace(key) && path.EndsWith(key, StringComparison.InvariantCultureIgnoreCase))
if (!string.IsNullOrWhiteSpace(key) && path.StartsWith(key, StringComparison.InvariantCultureIgnoreCase))
{
imageService = service;
}

8
src/ImageProcessor.Web/ImageProcessor.Web.csproj

@ -46,6 +46,13 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Caching\CachedImage.cs" />
<Compile Include="Caching\DiskCache.cs" />
<Compile Include="Caching\IImageCache.cs" />
<Compile Include="Caching\ImageCacheBase.cs" />
<Compile Include="Configuration\Shared\SettingElement.cs" />
<Compile Include="Configuration\Shared\SettingElementCollection.cs" />
<Compile Include="Extensions\TypeInitializationExtensions.cs" />
<Compile Include="Helpers\TypePropertyHelpers.cs" />
<Compile Include="Helpers\ProcessQueryStringEventArgs.cs" />
<Compile Include="Processors\DetectEdges.cs" />
<Compile Include="Processors\EntropyCrop.cs" />
@ -56,7 +63,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" />

2
src/ImageProcessor.Web/ImageProcessor.Web.csproj.DotSettings

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Cshared/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

4
src/ImageProcessor.Web/Services/IImageService.cs

@ -4,7 +4,7 @@
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Defines properties and methods for allowing retrieval of image from different sources.
// Defines properties and methods for allowing retrieval of images from different sources.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
@ -15,7 +15,7 @@ namespace ImageProcessor.Web.Services
using System.Threading.Tasks;
/// <summary>
/// Defines properties and methods for allowing retrieval of image from different sources.
/// Defines properties and methods for allowing retrieval of images from different sources.
/// </summary>
public interface IImageService
{

5
src/ImageProcessor.Web/Services/RemoteImageService.cs

@ -37,7 +37,8 @@ namespace ImageProcessor.Web.Services
this.Settings = new Dictionary<string, string>
{
{ "MaxBytes", "4194304" },
{ "Timeout", "30000" }
{ "Timeout", "30000" },
{ "Protocol", "http" }
};
this.WhiteList = new Uri[] { };
@ -105,7 +106,7 @@ namespace ImageProcessor.Web.Services
{
if (!uri.IsAbsoluteUri)
{
Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart(new[] { '.', '/' }));
Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart('.', '/'));
validUrl = upper.StartsWith(rebaseUri.Host.ToUpperInvariant()) || upper.EndsWith(rebaseUri.Host.ToUpperInvariant());
}
else

1
src/ImageProcessor.Web/Settings.StyleCop

@ -1,6 +1,7 @@
<StyleCopSettings Version="105">
<GlobalSettings>
<CollectionProperty Name="RecognizedWords">
<Value>cdn</Value>
<Value>dllimport</Value>
</CollectionProperty>
</GlobalSettings>

17
src/ImageProcessor.sln

@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Playground",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Web.PostProcessor", "ImageProcessor.Web.PostProcessor\ImageProcessor.Web.PostProcessor.csproj", "{55D08737-7D7E-4995-8892-BD9F944329E6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Web.AzureBlobCache", "ImageProcessor.Web.AzureBlobCache\ImageProcessor.Web.AzureBlobCache.csproj", "{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
All|Any CPU = All|Any CPU
@ -198,6 +200,21 @@ Global
{55D08737-7D7E-4995-8892-BD9F944329E6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{55D08737-7D7E-4995-8892-BD9F944329E6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{55D08737-7D7E-4995-8892-BD9F944329E6}.Release|x86.ActiveCfg = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Any CPU.ActiveCfg = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Any CPU.Build.0 = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Mixed Platforms.ActiveCfg = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Mixed Platforms.Build.0 = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|x86.ActiveCfg = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|x86.ActiveCfg = Debug|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Any CPU.Build.0 = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

1
src/ImageProcessor.sln.DotSettings

@ -18,5 +18,6 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=REF/@EntryIndexedValue">REF</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RGB/@EntryIndexedValue">RGB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RGBA/@EntryIndexedValue">RGBA</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SHA/@EntryIndexedValue">SHA</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SRGB/@EntryIndexedValue">SRGB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SS/@EntryIndexedValue">SS</s:String></wpf:ResourceDictionary>

6
src/ImageProcessor/ImageFactory.cs

@ -139,6 +139,9 @@ namespace ImageProcessor
/// </returns>
public ImageFactory Load(Stream stream)
{
// Reset the position of the stream to ensure we're reading the correct part.
stream.Position = 0;
ISupportedImageFormat format = FormatUtilities.GetFormat(stream);
if (format == null)
@ -1079,8 +1082,9 @@ namespace ImageProcessor
if (this.ShouldProcess)
{
// Allow the same stream to be used as for input.
stream.Position = 0;
stream.SetLength(0);
this.Image = this.CurrentImageFormat.Save(stream, this.Image);
stream.Position = 0;
}
return this;

1
src/ImageProcessor/Imaging/Formats/FormatBase.cs

@ -108,7 +108,6 @@ namespace ImageProcessor.Imaging.Formats
public virtual Image Save(Stream stream, Image image)
{
image.Save(stream, this.ImageFormat);
stream.Position = 0;
return image;
}

18
src/TestWebsites/MVC/Global.asax.cs

@ -28,17 +28,17 @@ namespace Test_Website_NET45
RouteConfig.RegisterRoutes(RouteTable.Routes);
// Test the post processing event.
ImageProcessingModule.OnPostProcessing += (sender, args) => Debug.WriteLine(args.CachedImagePath);
//ImageProcessingModule.OnPostProcessing += (sender, args) => Debug.WriteLine(args.CachedImagePath);
ImageProcessingModule.OnProcessQuerystring += (sender, args) =>
{
if (!args.RawUrl.Contains("penguins"))
{
return args.Querystring += "watermark=protected&color=fff&fontsize=36&fontopacity=70textshadow=true&fontfamily=arial";
}
//ImageProcessingModule.OnProcessQuerystring += (sender, args) =>
// {
// if (!args.RawUrl.Contains("penguins"))
// {
// return args.Querystring += "watermark=protected&color=fff&fontsize=36&fontopacity=70textshadow=true&fontfamily=arial";
// }
return args.Querystring;
};
// return args.Querystring;
// };
}
private async void WritePath(object sender, PostProcessingEventArgs e)

4
src/TestWebsites/MVC/Test_Website_MVC.csproj

@ -152,6 +152,10 @@
<Content Include="Views\Home\Index.cshtml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ImageProcessor.Web.AzureBlobCache\ImageProcessor.Web.AzureBlobCache.csproj">
<Project>{3c805e4c-d679-43f8-8c43-8909cdb4d4d7}</Project>
<Name>ImageProcessor.Web.AzureBlobCache</Name>
</ProjectReference>
<ProjectReference Include="..\..\ImageProcessor.Web.PostProcessor\ImageProcessor.Web.PostProcessor.csproj">
<Project>{55d08737-7d7e-4995-8892-bd9f944329e6}</Project>
<Name>ImageProcessor.Web.PostProcessor</Name>

19
src/TestWebsites/MVC/Views/Home/Index.cshtml

@ -7,24 +7,25 @@
<div class="row">
<div class="col-s-6">
<h2>Resized</h2>
<img src="/images/format-Penguins.jpg?width=300" />
<h3>Foreign language test.</h3>
<img src="/images/format.Penguins.jpg?width=302"/>
<img src="/remote.axd/ipcache.blob.core.windows.net/source/IMG_0671.JPG?width=302&filter=comic" />
@*<h3>Foreign language test.</h3>
<img src="/images/udendørs.jpg?width=300" />
<img src="/images/udendørs.jpg?preset=demo&filter=comic" />
<h3>Strange name</h3>
<img src="/images/falahill_design__160p.jpg?preset=demo" />
<img src="/images/falahill_design__160p.jpg?preset=demo" />*@
</div>
<div class="col-s-6">
@*<div class="col-s-6">
<h2>Cropped </h2>
<img src="/images/format-Penguins.jpg?crop=0,0,300,225" />
<h3>Cropped Percent</h3>
<img src="/images/format-Penguins.jpg?width=300&height300&crop=0.1,0.2,0.1,0.6&cropmode=percent" />
<img src="/images/bus.jpg?width=311" />
</div>
</div>*@
</div>
</section>
<section>
@*<section>
<div class="row">
<h2>Reside Pad</h2>
<div class="col-s-4">
@ -199,9 +200,9 @@
<img src="/images/format-Penguins.jpg?width=300&tint=af76a6" />
</div>
</div>
</section>
</section>*@
</article>
<article>
@*<article>
<h1>Color Profiles</h1>
<section>
<div class="row">
@ -223,4 +224,4 @@
<img src="/Images/header_1.jpg?width=750&crop=0-48-750-220" />
</div>
</section>
</article>
</article>*@

4
src/TestWebsites/MVC/Web.config

@ -9,12 +9,12 @@
<sectionGroup name="imageProcessor">
<section name="security" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageSecuritySection, ImageProcessor.Web" />
<section name="processing" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageProcessingSection, ImageProcessor.Web" />
<section name="cache" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageCacheSection, ImageProcessor.Web" />
<section name="caching" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageCacheSection, ImageProcessor.Web" />
</sectionGroup>
</configSections>
<imageProcessor>
<security configSource="config\imageprocessor\security.config" />
<cache configSource="config\imageprocessor\cache.config" />
<caching configSource="config\imageprocessor\cache.config" />
<processing configSource="config\imageprocessor\processing.config" />
</imageProcessor>
<appSettings>

12
src/TestWebsites/MVC/config/imageprocessor/cache.config

@ -1,3 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<cache virtualPath="~/app_data/cache" maxDays="56"/>
<caching currentCache="DiskCache">
<caches>
<cache name="DiskCache" type="ImageProcessor.Web.Caching.DiskCache, ImageProcessor.Web">
<settings>
<setting key="MaxDays" value="56"/>
<setting key="VirtualCachePath" value="~/app_data/cache"/>
</settings>
</cache>
</caches>
</caching>

2
src/TestWebsites/MVC/config/imageprocessor/security.config

@ -6,8 +6,10 @@
<settings>
<setting key="MaxBytes" value="4194304"/>
<setting key="Timeout" value="30000"/>
<setting key="Protocol" value="http"/>
</settings>
<whitelist>
<add url="http://ipcache.blob.core.windows.net/"/>
<add url="images.mymovies.net"/>
<add url="http://maps.googleapis.com"/>
<add url="fbcdn"/>

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