// ----------------------------------------------------------------------- // // TODO: Update copyright text. // // ----------------------------------------------------------------------- namespace ImageProcessor.Web.Caching { #region Using using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using ImageProcessor.Helpers.Extensions; using ImageProcessor.Web.Config; using ImageProcessor.Web.Helpers; #endregion /// /// Encapsulates methods to handle disk caching of images. /// internal sealed class DiskCache { #region Fields /// /// The maximum number or time a new file should be cached before checking the /// cache controller and running any clearing mechanisms. /// /// /// NTFS file systems can handle up to 8000 files in one directory. The Cache controller will clear out any /// time we hit 6000 so if we tell the handler to run at every 1000 times an image is added to the cache we /// should have a 1000 file buffer. /// internal const int MaxRunsBeforeCacheClear = 1000; /// /// 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 Folder can handle up to 8000 files in a directory. /// This buffer will help us to ensure that we rarely hit anywhere near that limit. /// private const int MaxFilesCount = 6000; /// /// The regular expression to search strings for extension changes. /// private static readonly Regex FormatRegex = new Regex(@"format=(jpeg|png|bmp|gif)", RegexOptions.Compiled); /// /// The object to lock against. /// private static readonly object SyncRoot = new object(); /// /// 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) { DirectoryInfo di = new DirectoryInfo(absoluteCachePath); if (!di.Exists) { // Create the directory. Directory.CreateDirectory(absoluteCachePath); } string parsedExtension = ParseExtension(imagePath); string fallbackExtension = imageName.Substring(imageName.LastIndexOf(".", StringComparison.Ordinal)); string cachedFileName = string.Format( "{0}{1}", imagePath.ToMD5Fingerprint(), !string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension : fallbackExtension); cachedPath = Path.Combine(absoluteCachePath, cachedFileName); } return cachedPath; } /// /// 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."); } /// /// Purges any files from the filesystem cache in a background thread. /// internal static void PurgeCachedFolders() { ThreadStart threadStart = PurgeFolders; Thread thread = new Thread(threadStart) { IsBackground = true }; thread.Start(); } /// /// 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) { if (File.Exists(imagePath) && File.Exists(cachedImagePath)) { FileInfo imageFileInfo = new FileInfo(imagePath); FileInfo cachedImageFileInfo = new FileInfo(cachedImagePath); return !new FileCompareLastwritetime().Equals(imageFileInfo, cachedImageFileInfo); } return true; } /// /// Sets the LastWriteTime of the cached file to match the original file. /// /// The original image path. /// The cached image path. internal static void SetCachedLastWriteTime(string imagePath, string cachedImagePath) { if (File.Exists(imagePath) && File.Exists(cachedImagePath)) { lock (SyncRoot) { DateTime dateTime = File.GetLastWriteTime(imagePath); File.SetLastWriteTime(cachedImagePath, dateTime); } } } /// /// Purges any files from the filesystem cache in the given folders. /// private static void PurgeFolders() { string folder = HostingEnvironment.MapPath(CachePath); if (folder != null) { DirectoryInfo directoryInfo = new DirectoryInfo(folder); if (directoryInfo.Exists) { // Get all the files in the cache ordered by LastAccessTime - oldest first. List fileInfos = directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories) .OrderBy(x => x.LastAccessTime).ToList(); int counter = fileInfos.Count; Parallel.ForEach( fileInfos, fileInfo => { lock (SyncRoot) { try { // Delete the file if we are nearing our limit buffer. if (counter >= MaxFilesCount || fileInfo.LastAccessTime < DateTime.Now.AddDays(-MaxFileCachedDuration)) { fileInfo.Delete(); counter -= 1; } } catch { // TODO: Sort out the try/catch. throw; } } }); } } } /// /// 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) { foreach (Match match in FormatRegex.Matches(input)) { if (match.Success) { return "." + match.Value.Split('=')[1]; } } return string.Empty; } #endregion } }