// ----------------------------------------------------------------------- // // 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 } }