@ -10,9 +10,11 @@ namespace ImageProcessor.Web.Caching
#region Using
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Text.RegularExpressions ;
using System.Threading.Tasks ;
using System.Web ;
using System.Web.Hosting ;
using ImageProcessor.Helpers.Extensions ;
@ -26,67 +28,109 @@ namespace ImageProcessor.Web.Caching
{
#region Fields
/// <summary>
/// The maximum number of days to cache files on the system for.
/// The maximum number of days to cache files on the system for.
/// </summary>
internal static readonly int MaxFileCachedDuration = ImageProcessorConfig . Instance . MaxCacheDays ;
/// <summary>
/// The valid sub directory chars. This used in combination with the file limit per folder
/// allows the storage of 360,000 image files in the cache.
/// </summary>
private const string ValidSubDirectoryChars = "abcdefghijklmnopqrstuvwxyz0123456789" ;
/// <summary>
/// The maximum number of files allowed in the directory.
/// </summary>
/// <remarks>
/// NTFS directories can handle up to 8000 files in the directory before slowing down.
/// This buffer will help us to ensure that we rarely hit anywhere near that limit.
/// 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 cref="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
/// <see cref="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
/// <see cref="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
/// </remarks>
private const int MaxFilesCount = 7 5 0 0 ;
private const int MaxFilesCount = 1 0 0 0 0 ;
/// <summary>
/// The regular expression to search strings for file extensions.
/// </summary>
private static readonly Regex FormatRegex = new Regex (
@"(jpeg|png|bmp|gif)" , RegexOptions . RightToLeft | RegexOptions . Compiled ) ;
/// <summary>
/// The regular expression to search strings for extension changes.
/// The regular expression to search strings for valid subfolder names.
/// We're specifically not using a shorter regex as we need to be able to iterate through
/// each match group.
/// </summary>
private static readonly Regex FormatRegex = new Regex ( @"(jpeg|png|bmp|gif)" , RegexOptions . RightToLeft | RegexOptions . Compiled ) ;
private static readonly Regex Sub Folde rRegex = new Regex ( @"(\/(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|0|1|2|3|4|5|6|7|8|9)\/)" , RegexOptions . Compiled ) ;
/// <summary>
/// The default paths for Cached folders on the server.
/// The absolute path to virtual cache path on the server.
/// </summary>
private static readonly string CachePath = ImageProcessorConfig . Instance . VirtualCachePath ;
private static readonly string AbsoluteCachePath =
HostingEnvironment . MapPath ( ImageProcessorConfig . Instance . VirtualCachePath ) ;
#endregion
#region Methods
/// <summary>
/// The create cache paths.
/// </summary>
/// <returns>
/// The true if the cache directories are created successfully; otherwise, false.
/// </returns>
internal static bool CreateCacheDirectories ( )
{
try
{
Parallel . ForEach (
ValidSubDirectoryChars . ToCharArray ( ) ,
( extension , loop ) = >
{
string path = Path . Combine ( AbsoluteCachePath , extension . ToString ( CultureInfo . InvariantCulture ) ) ;
DirectoryInfo directoryInfo = new DirectoryInfo ( path ) ;
if ( ! directoryInfo . Exists )
{
directoryInfo . Create ( ) ;
}
} ) ;
}
catch
{
return false ;
}
return true ;
}
/// <summary>
/// Gets the full transformed cached path for the image.
/// The file names are stored as MD5 encrypted versions of the full request path.
/// This should make them unique enough to
/// </summary>
/// <param name="imagePath">The original image path.</param>
/// <param name="imageName">The original image name.</param>
/// <returns>The full cached path for the image.</returns>
internal static string GetCachePath ( string imagePath , string imageName )
{
string virtualCachePath = CachePath ;
string absoluteCachePath = HostingEnvironment . MapPath ( virtualCachePath ) ;
string cachedPath = string . Empty ;
if ( absoluteCachePath ! = null )
if ( A bsoluteCachePath ! = null )
{
// Use an md5 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 first character of that hash as a subfolder.
string parsedExtension = ParseExtension ( imagePath ) ;
string fallbackExtension = imageName . Substring ( imageName . LastIndexOf ( "." , StringComparison . Ordinal ) + 1 ) ;
string subpath = ! string . IsNullOrWhiteSpace ( parsedExtension ) ? parsedExtension : fallbackExtension ;
string encryptedName = imagePath . ToMD5Fingerprint ( ) ;
string subpath = encryptedName . Substring ( 0 , 1 ) ;
string cachedFileName = string . Format (
"{0}.{1}" ,
imagePath . ToMD5Fingerprint ( ) ,
encryptedName ,
! string . IsNullOrWhiteSpace ( parsedExtension ) ? parsedExtension : fallbackExtension ) ;
cachedPath = Path . Combine ( absoluteCachePath , subpath , cachedFileName ) ;
string cachedDirectory = Path . GetDirectoryName ( cachedPath ) ;
if ( cachedDirectory ! = null )
{
DirectoryInfo directoryInfo = new DirectoryInfo ( cachedDirectory ) ;
if ( ! directoryInfo . Exists )
{
// Create the directory.
directoryInfo . Create ( ) ;
}
}
cachedPath = Path . Combine ( AbsoluteCachePath , subpath , cachedFileName ) ;
}
return cachedPath ;
@ -166,10 +210,12 @@ namespace ImageProcessor.Web.Caching
return true ;
}
FileInfo imageFileInfo = new FileInfo ( imagePath ) ;
if ( imageFileInfo . Exists )
// Test now for locally requested files.
if ( PersistantDictionary . Instance . TryGetValue ( key , out cachedImage ) )
{
if ( PersistantDictionary . Instance . TryGetValue ( key , out cachedImage ) )
FileInfo imageFileInfo = new FileInfo ( imagePath ) ;
if ( imageFileInfo . Exists )
{
// Check to see if the last write time is different of whether the
// cached image is set to expire or if the max age is different.
@ -183,11 +229,11 @@ namespace ImageProcessor.Web.Caching
}
}
}
else
{
// Nothing in the cache so we should return true.
return true ;
}
}
else
{
// Nothing in the cache so we should return true.
return true ;
}
return false ;
@ -239,7 +285,7 @@ namespace ImageProcessor.Web.Caching
// Group each cache folder and clear any expired items or any that exeed
// the maximum allowable count.
var groups = PersistantDictionary . Instance . ToList ( )
. GroupBy ( x = > Format Regex . Match ( x . Value . Path ) . Value )
. GroupBy ( x = > Sub Folde rRegex. Match ( x . Value . Path ) . Value )
. Where ( g = > g . Count ( ) > MaxFilesCount ) ;
foreach ( var group in groups )
@ -269,11 +315,10 @@ namespace ImageProcessor.Web.Caching
groupCount - = 1 ;
}
}
catch ( Exception ex )
catch ( Exception )
{
// Do Nothing, skip to the next.
// TODO: Should we handle this?
throw ex ;
continue ;
}
}