// -----------------------------------------------------------------------
//
// Copyright (c) James South.
// Dual licensed under the MIT or GPL Version 2 licenses.
//
// -----------------------------------------------------------------------
namespace ImageProcessor.Web.Caching
{
#region Using
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Hosting;
using ImageProcessor.Helpers.Extensions;
using ImageProcessor.Web.Config;
#endregion
///
/// Encapsulates methods to handle disk caching of images.
///
internal sealed class DiskCache
{
#region Fields
///
/// The maximum number of days to cache files on the system for.
///
internal static readonly int MaxFileCachedDuration = ImageProcessorConfig.Instance.MaxCacheDays;
///
/// The maximum number of files allowed in the directory.
///
///
/// 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.
///
private const int MaxFilesCount = 7500;
///
/// The regular expression to search strings for extension changes.
///
private static readonly Regex FormatRegex = new Regex(@"(jpeg|png|bmp|gif)", RegexOptions.RightToLeft | RegexOptions.Compiled);
///
/// The default paths for Cached folders on the server.
///
private static readonly string CachePath = ImageProcessorConfig.Instance.VirtualCachePath;
#endregion
#region Methods
///
/// Gets the full transformed cached path for the image.
///
/// The original image path.
/// The original image name.
/// The full cached path for the image.
internal static string GetCachePath(string imagePath, string imageName)
{
string virtualCachePath = CachePath;
string absoluteCachePath = HostingEnvironment.MapPath(virtualCachePath);
string cachedPath = string.Empty;
if (absoluteCachePath != null)
{
string parsedExtension = ParseExtension(imagePath);
string fallbackExtension = imageName.Substring(imageName.LastIndexOf(".", StringComparison.Ordinal) + 1);
string subpath = !string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension : fallbackExtension;
string cachedFileName = string.Format(
"{0}.{1}",
imagePath.ToMD5Fingerprint(),
!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.
Directory.CreateDirectory(cachedDirectory);
}
}
}
return cachedPath;
}
///
/// Adds an image to the cache.
///
///
/// The cached path.
///
///
/// The last write time.
///
internal static void AddImageToCache(string cachedPath, DateTime lastWriteTimeUtc)
{
string key = Path.GetFileNameWithoutExtension(cachedPath);
DateTime expires = DateTime.UtcNow.AddDays(MaxFileCachedDuration).ToUniversalTime();
CachedImage cachedImage = new CachedImage(cachedPath, MaxFileCachedDuration, lastWriteTimeUtc, expires);
PersistantDictionary.Instance.Add(key, cachedImage);
}
///
/// Converts an absolute file path
///
/// The absolute path to convert.
/// The from the current context.
/// The virtual path to the file.
internal static string GetVirtualPath(string absolutePath, HttpRequest request)
{
string applicationPath = request.PhysicalApplicationPath;
string virtualDir = request.ApplicationPath;
virtualDir = virtualDir == "/" ? virtualDir : (virtualDir + "/");
if (applicationPath != null)
{
return absolutePath.Replace(applicationPath, virtualDir).Replace(@"\", "/");
}
throw new InvalidOperationException("We can only map an absolute back to a relative path if the application path is available.");
}
///
/// Returns a value indicating whether the original file has been updated.
///
/// The original image path.
/// The cached image path.
///
/// True if the the original file has been updated; otherwise, false.
///
internal static bool IsUpdatedFile(string imagePath, string cachedImagePath)
{
string key = Path.GetFileNameWithoutExtension(cachedImagePath);
bool isUpdated = false;
if (File.Exists(imagePath))
{
FileInfo imageFileInfo = new FileInfo(imagePath);
CachedImage cachedImage;
if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
{
// 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.
if (imageFileInfo.LastWriteTimeUtc != cachedImage.LastWriteTimeUtc
|| cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration)
|| cachedImage.MaxAge != MaxFileCachedDuration)
{
if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
{
isUpdated = true;
}
}
}
}
return isUpdated;
}
///
/// Sets the LastWriteTime of the cached file to match the original file.
///
///
/// The original image path.
///
///
/// The cached image path.
///
///
/// The set to the last write time of the file.
///
internal static DateTime SetCachedLastWriteTime(string imagePath, string cachedImagePath)
{
if (File.Exists(imagePath) && File.Exists(cachedImagePath))
{
DateTime dateTime = File.GetLastWriteTimeUtc(imagePath);
File.SetLastWriteTimeUtc(cachedImagePath, dateTime);
return dateTime;
}
return DateTime.MinValue.ToUniversalTime();
}
///
/// Purges any files from the file-system cache in the given folders.
///
internal static void TrimCachedFolders()
{
// Group each cache folder and clear any expired items or any that exeed
// the maximum allowable count.
var groups = PersistantDictionary.Instance.ToList()
.GroupBy(x => FormatRegex.Match(x.Value.Path).Value)
.Where(g => g.Count() > MaxFilesCount);
foreach (var group in groups)
{
int groupCount = group.Count();
foreach (KeyValuePair pair in group.OrderBy(x => x.Value.ExpiresUtc))
{
// If the group count is equal to the max count minus 1 then we know we
// are counting down from a full directory not simply clearing out
// expired items.
if (groupCount == MaxFilesCount - 1)
{
break;
}
try
{
// Remove from the cache and delete each CachedImage.
FileInfo fileInfo = new FileInfo(pair.Value.Path);
string key = Path.GetFileNameWithoutExtension(fileInfo.Name);
CachedImage cachedImage;
if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
{
fileInfo.Delete();
groupCount -= 1;
}
}
catch (Exception)
{
// Do Nothing, skip to the next.
// TODO: Should we handle this?
continue;
}
}
}
}
///
/// Returns the correct file extension for the given string input
///
///
/// The string to parse.
///
///
/// The correct file extension for the given string input if it can find one; otherwise an empty string.
///
private static string ParseExtension(string input)
{
Match match = FormatRegex.Match(input);
return match.Success ? match.Value : string.Empty;
}
#endregion
}
}