// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // Provides an implementation that is file system based. // The cache is self healing and cleaning. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Web.Caching { using System; using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using ImageProcessor.Web.Extensions; /// /// Provides an implementation that is file system based. /// The cache is self healing and cleaning. /// public class DiskCache : ImageCacheBase { /// /// 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 maximum number of days to store the image. /// private readonly int maxDays; /// /// The virtual cache path. /// private readonly string virtualCachePath; /// /// The absolute path to virtual cache path on the server. /// private readonly string absoluteCachePath; /// /// The virtual path to the cached file. /// private string virtualCachedFilePath; /// /// 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) : base(requestPath, fullPath, querystring) { this.maxDays = Convert.ToInt32(this.Settings["MaxDays"]); string virtualPath = this.Settings["VirtualCachePath"]; if (!virtualPath.IsValidVirtualPathName()) { throw new ConfigurationErrorsException("DiskCache 'VirtualCachePath' is not a valid virtual path."); } this.virtualCachePath = virtualPath; this.absoluteCachePath = HostingEnvironment.MapPath(this.virtualCachePath); } /// /// Gets the maximum number of days to store the image. /// public override int MaxDays { get { return this.maxDays; } } /// /// Gets a value indicating whether the image is new or updated in an asynchronous manner. /// /// /// The . /// public override async Task IsNewOrUpdatedAsync() { string cachedFileName = await this.CreateCachedFileName(); // Collision rate of about 1 in 10000 for the folder structure. // That gives us massive scope to store millions of files. string pathFromKey = string.Join("\\", cachedFileName.ToCharArray().Take(6)); string virtualPathFromKey = pathFromKey.Replace(@"\", "/"); this.CachedPath = Path.Combine(this.absoluteCachePath, pathFromKey, cachedFileName); this.virtualCachedFilePath = Path.Combine(this.virtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/"); bool isUpdated = false; CachedImage cachedImage = CacheIndexer.GetValue(this.CachedPath); if (cachedImage == null) { FileInfo fileInfo = new FileInfo(this.CachedPath); if (fileInfo.Exists) { // Pull the latest info. fileInfo.Refresh(); cachedImage = new CachedImage { Key = Path.GetFileNameWithoutExtension(this.CachedPath), Path = this.CachedPath, CreationTimeUtc = fileInfo.CreationTimeUtc }; CacheIndexer.Add(cachedImage); } } 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 (this.IsExpired(cachedImage.CreationTimeUtc)) { CacheIndexer.Remove(this.CachedPath); isUpdated = true; } } return isUpdated; } /// /// Adds the image to the cache in an asynchronous manner. /// /// /// The stream containing the image data. /// /// /// The content type of the image. /// /// /// The representing an asynchronous operation. /// public override async Task AddImageToCacheAsync(Stream stream, string contentType) { // ReSharper disable once AssignNullToNotNullAttribute DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.CachedPath)); if (!directoryInfo.Exists) { directoryInfo.Create(); } using (FileStream fileStream = File.Create(this.CachedPath)) { await stream.CopyToAsync(fileStream); } } /// /// Trims the cache of any expired items in an asynchronous manner. /// /// /// The asynchronous representing an asynchronous operation. /// public override async Task TrimCacheAsync() { string directory = Path.GetDirectoryName(this.CachedPath); 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 await parentDirectoryInfo.SafeEnumerateDirectoriesAsync()) { 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 (!this.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. } } } } } } /// /// Rewrites the path to point to the cached image. /// /// /// The encapsulating all information about the request. /// public override void RewritePath(HttpContext context) { // The cached file is valid so just rewrite the path. context.RewritePath(this.virtualCachedFilePath, false); } } }