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