// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // The disk cache. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Web.Caching { #region Using using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Web.Hosting; using ImageProcessor.Web.Configuration; using ImageProcessor.Web.Extensions; using ImageProcessor.Web.Helpers; #endregion /// /// The disk cache. /// internal sealed class DiskCache { #region Fields /// /// The maximum number of days to cache files on the system for. /// internal static readonly int MaxFileCachedDuration = ImageProcessorConfiguration.Instance.MaxCacheDays; /// /// The maximum number of files allowed in the directory. /// /// /// 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. /// /// /// /// private const int MaxFilesCount = 100; /// /// The virtual cache path. /// private static readonly string VirtualCachePath = ImageProcessorConfiguration.Instance.VirtualCachePath; /// /// The absolute path to virtual cache path on the server. /// private static readonly string AbsoluteCachePath = HostingEnvironment.MapPath(ImageProcessorConfiguration.Instance.VirtualCachePath); /// /// The request path for the image. /// private readonly string requestPath; /// /// The full path for the image. /// private readonly string fullPath; /// /// The querystring containing processing instructions. /// private readonly string querystring; /// /// The physical cached path. /// private string physicalCachedPath; /// /// The virtual cached path. /// private string virtualCachedPath; #endregion #region Constructors /// /// Initializes a new instance of the class. /// /// /// The request path for the image. /// /// /// The full path for the image. /// /// /// The querystring containing instructions. /// public DiskCache(string requestPath, string fullPath, string querystring) { this.requestPath = requestPath; this.fullPath = fullPath; this.querystring = querystring; // Get the physical and virtual paths. this.GetCachePaths(); } #endregion /// /// Gets the cached path. /// public string CachedPath { get { return this.physicalCachedPath; } } /// /// Gets the cached path. /// public string VirtualCachedPath { get { return this.virtualCachedPath; } } #region Methods #region Public /// /// Trims a cached folder ensuring that it does not exceed the maximum file count. /// /// /// The path to the folder. /// public static void TrimCachedFolders(string path) { string directory = Path.GetDirectoryName(path); 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 parentDirectoryInfo.SafeEnumerateDirectories()) { IEnumerable 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 (!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. } } } } } } /// /// Adds an image to the cache. /// /// /// The path to the cached image. /// public void AddImageToCache(string cachedPath) { string key = Path.GetFileNameWithoutExtension(cachedPath); CachedImage cachedImage = new CachedImage { Key = key, Path = cachedPath, CreationTimeUtc = DateTime.UtcNow }; CacheIndexer.Add(cachedImage); } /// /// Returns a value indicating whether the original file is new or has been updated. /// /// /// The path to the cached image. /// /// /// True if The original file is new or has been updated; otherwise, false. /// public bool IsNewOrUpdatedFile(string cachedPath) { 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 /// /// Gets a value indicating whether the given images creation date is out with /// the prescribed limit. /// /// /// The creation date. /// /// /// The true if the date is out with the limit, otherwise; false. /// private static bool IsExpired(DateTime creationDate) { return creationDate.AddDays(MaxFileCachedDuration) < DateTime.UtcNow.AddDays(-MaxFileCachedDuration); } /// /// 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. /// 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(@"\", "/"); } } #endregion #endregion } }