mirror of https://github.com/SixLabors/ImageSharp
Browse Source
Former-commit-id: 1ba1d6f6fdba545f9659d634cc9bb1e6e0833758 Former-commit-id: 192eccd79658cb84e31c22e23c0f4c9690dbe314af/merge-core
21 changed files with 775 additions and 804 deletions
@ -1,168 +0,0 @@ |
|||
namespace ImageProcessor.Web.Caching |
|||
{ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using System.Web; |
|||
using System.Web.Hosting; |
|||
|
|||
using ImageProcessor.Web.Configuration; |
|||
using ImageProcessor.Web.Extensions; |
|||
|
|||
public class DiskCache2 : ImageCacheBase |
|||
{ |
|||
/// <summary>
|
|||
/// The maximum number of files allowed in the directory.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// NTFS directories can handle up to 10,000 files in the directory before slowing down.
|
|||
/// This will help us to ensure that don't go over that limit.
|
|||
/// <see href="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
|
|||
/// <see href="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
|
|||
/// <see href="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
|
|||
/// </remarks>
|
|||
private const int MaxFilesCount = 100; |
|||
|
|||
/// <summary>
|
|||
/// The max age.
|
|||
/// </summary>
|
|||
private readonly int maxAge; |
|||
|
|||
/// <summary>
|
|||
/// The virtual cache path.
|
|||
/// </summary>
|
|||
private readonly string virtualCachePath; |
|||
|
|||
/// <summary>
|
|||
/// The absolute path to virtual cache path on the server.
|
|||
/// </summary>
|
|||
private readonly string absoluteCachePath; |
|||
|
|||
/// <summary>
|
|||
/// The virtual cached path to the cached file.
|
|||
/// </summary>
|
|||
private string virtualCachedFilePath; |
|||
|
|||
public DiskCache2(string requestPath, string fullPath, string querystring) |
|||
: base(requestPath, fullPath, querystring) |
|||
{ |
|||
// TODO: Get from configuration.
|
|||
this.Settings = new Dictionary<string, string>(); |
|||
this.maxAge = Convert.ToInt32(this.Settings["MaxAge"]); |
|||
this.virtualCachePath = this.Settings["VirtualCachePath"]; |
|||
this.absoluteCachePath = HostingEnvironment.MapPath(this.virtualCachePath); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The maximum number of days to cache files on the system for.
|
|||
/// TODO: Shift the getter source to proper config.
|
|||
/// </summary>
|
|||
public override int MaxAge |
|||
{ |
|||
get |
|||
{ |
|||
return this.maxAge; |
|||
} |
|||
} |
|||
|
|||
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)); |
|||
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) |
|||
{ |
|||
// 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; |
|||
} |
|||
|
|||
public override async Task AddImageToCacheAsync(Stream stream) |
|||
{ |
|||
// 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); |
|||
} |
|||
} |
|||
|
|||
public override async Task TrimCacheAsync() |
|||
{ |
|||
string directory = Path.GetDirectoryName(this.CachedPath); |
|||
|
|||
if (directory != null) |
|||
{ |
|||
DirectoryInfo directoryInfo = new DirectoryInfo(directory); |
|||
DirectoryInfo parentDirectoryInfo = directoryInfo.Parent; |
|||
|
|||
if (parentDirectoryInfo != null) |
|||
{ |
|||
// UNC folders can throw exceptions if the file doesn't exist.
|
|||
foreach (DirectoryInfo enumerateDirectory in await parentDirectoryInfo.SafeEnumerateDirectoriesAsync()) |
|||
{ |
|||
IEnumerable<FileInfo> files = enumerateDirectory.EnumerateFiles().OrderBy(f => f.CreationTimeUtc); |
|||
int count = files.Count(); |
|||
|
|||
foreach (FileInfo fileInfo in files) |
|||
{ |
|||
try |
|||
{ |
|||
// If the group count is equal to the max count minus 1 then we know we
|
|||
// have reduced the number of items below the maximum allowed.
|
|||
// We'll cleanup any orphaned expired files though.
|
|||
if (!this.IsExpired(fileInfo.CreationTimeUtc) && count <= MaxFilesCount - 1) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
// Remove from the cache and delete each CachedImage.
|
|||
CacheIndexer.Remove(fileInfo.Name); |
|||
fileInfo.Delete(); |
|||
count -= 1; |
|||
} |
|||
// ReSharper disable once EmptyGeneralCatchClause
|
|||
catch |
|||
{ |
|||
// Do nothing; skip to the next file.
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public override void RewritePath(HttpContext context) |
|||
{ |
|||
// The cached file is valid so just rewrite the path.
|
|||
context.RewritePath(this.virtualCachedFilePath, false); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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="MaxAge" value="56"/> |
|||
<setting key="VirtualCachePath" value="~/app_data/cache"/> |
|||
</settings> |
|||
</cache> |
|||
</caches> |
|||
</caching> |
|||
|
|||
|
|||
Loading…
Reference in new issue