mirror of https://github.com/SixLabors/ImageSharp
4 changed files with 911 additions and 0 deletions
@ -0,0 +1,389 @@ |
|||||
|
|
||||
|
|
||||
|
namespace ImageProcessor.Web.Caching |
||||
|
{ |
||||
|
#region Using
|
||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.IO; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Web; |
||||
|
using System.Web.Hosting; |
||||
|
using ImageProcessor.Helpers.Extensions; |
||||
|
using ImageProcessor.Web.Config; |
||||
|
#endregion
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The cache.
|
||||
|
/// </summary>
|
||||
|
internal sealed class Cache |
||||
|
{ |
||||
|
#region Fields
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum number of days to cache files on the system for.
|
||||
|
/// </summary>
|
||||
|
internal static readonly int MaxFileCachedDuration = ImageProcessorConfig.Instance.MaxCacheDays; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The valid sub directory chars. This used in combination with the file limit per folder
|
||||
|
/// allows the storage of 360,000 image files in the cache.
|
||||
|
/// </summary>
|
||||
|
private const string ValidSubDirectoryChars = "abcdefghijklmnopqrstuvwxyz0123456789"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum number of files allowed in the directory.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 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.
|
||||
|
/// <see cref="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
|
||||
|
/// <see cref="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
|
||||
|
/// <see cref="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
|
||||
|
/// </remarks>
|
||||
|
private const int MaxFilesCount = 10000; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The regular expression to search strings for file extensions.
|
||||
|
/// </summary>
|
||||
|
private static readonly Regex FormatRegex = new Regex( |
||||
|
@"(jpeg|png|bmp|gif)", RegexOptions.RightToLeft | RegexOptions.Compiled); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The regular expression to search strings for valid subfolder names.
|
||||
|
/// We're specifically not using a shorter regex as we need to be able to iterate through
|
||||
|
/// each match group.
|
||||
|
/// </summary>
|
||||
|
private static readonly Regex SubFolderRegex = |
||||
|
new Regex( |
||||
|
@"(\/(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|0|1|2|3|4|5|6|7|8|9)\/)", |
||||
|
RegexOptions.Compiled); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The absolute path to virtual cache path on the server.
|
||||
|
/// </summary>
|
||||
|
private static readonly string AbsoluteCachePath = |
||||
|
HostingEnvironment.MapPath(ImageProcessorConfig.Instance.VirtualCachePath); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The concurrent dictionary.
|
||||
|
/// </summary>
|
||||
|
private ConcurrentDictionary<string, CachedImage> concurrentDictionary = |
||||
|
new ConcurrentDictionary<string, CachedImage>(); |
||||
|
|
||||
|
#endregion
|
||||
|
|
||||
|
#region Methods
|
||||
|
|
||||
|
public void Test() |
||||
|
{ |
||||
|
Task<bool> task = this.CreateDirectoriesAsync(); |
||||
|
if (task.Result) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
#region Internal
|
||||
|
/// <summary>
|
||||
|
/// Converts an absolute file path
|
||||
|
/// </summary>
|
||||
|
/// <param name="absolutePath">The absolute path to convert.</param>
|
||||
|
/// <param name="request">The <see cref="T:System.Web.HttpRequest"/>from the current context.</param>
|
||||
|
/// <returns>The virtual path to the file.</returns>
|
||||
|
internal 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."); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the full transformed cached path for the image.
|
||||
|
/// The file names are stored as MD5 encrypted versions of the full request path.
|
||||
|
/// This should make them unique enough to
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">The original image path.</param>
|
||||
|
/// <param name="imageName">The original image name.</param>
|
||||
|
/// <returns>The full cached path for the image.</returns>
|
||||
|
internal string GetCachePath(string imagePath, string imageName) |
||||
|
{ |
||||
|
string cachedPath = string.Empty; |
||||
|
|
||||
|
if (AbsoluteCachePath != null) |
||||
|
{ |
||||
|
// Use an md5 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 first character of that hash as a subfolder.
|
||||
|
string parsedExtension = this.ParseExtension(imagePath); |
||||
|
string fallbackExtension = imageName.Substring(imageName.LastIndexOf(".", StringComparison.Ordinal) + 1); |
||||
|
string encryptedName = imagePath.ToMD5Fingerprint(); |
||||
|
string subpath = encryptedName.Substring(0, 1); |
||||
|
|
||||
|
string cachedFileName = string.Format( |
||||
|
"{0}.{1}", |
||||
|
encryptedName, |
||||
|
!string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension : fallbackExtension); |
||||
|
|
||||
|
cachedPath = Path.Combine(AbsoluteCachePath, subpath, cachedFileName); |
||||
|
} |
||||
|
|
||||
|
return cachedPath; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates the cache directories for storing images.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// The true if the cache directories are created successfully; otherwise, false.
|
||||
|
/// </returns>
|
||||
|
internal /*async*/ Task<bool> CreateDirectoriesAsync() |
||||
|
{ |
||||
|
return this.CreateDirectoriesAsyncTasks().ToTask<bool>(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds an image to the cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cachedPath">
|
||||
|
/// The cached path.
|
||||
|
/// </param>
|
||||
|
/// <param name="lastWriteTimeUtc">
|
||||
|
/// The last write time.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The task.
|
||||
|
/// </returns>
|
||||
|
internal Task /*async*/ AddImageToCacheAsync(string cachedPath, DateTime lastWriteTimeUtc) |
||||
|
{ |
||||
|
return this.AddImageToCacheAsyncTask(cachedPath, lastWriteTimeUtc).ToTask(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a value indicating whether the original file has been updated.
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">The original image path.</param>
|
||||
|
/// <param name="cachedImagePath">The cached image path.</param>
|
||||
|
/// <param name="isRemote">Whether the file is a remote request.</param>
|
||||
|
/// <returns>
|
||||
|
/// True if the the original file has been updated; otherwise, false.
|
||||
|
/// </returns>
|
||||
|
internal /*async*/ Task<bool> IsUpdatedFileAsync(string imagePath, string cachedImagePath, bool isRemote) |
||||
|
{ |
||||
|
return this.IsUpdatedFileAsyncTask(imagePath, cachedImagePath, isRemote).ToTask<bool>(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the LastWriteTime of the cached file to match the original file.
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">
|
||||
|
/// The original image path.
|
||||
|
/// </param>
|
||||
|
/// <param name="cachedImagePath">
|
||||
|
/// The cached image path.
|
||||
|
/// </param>
|
||||
|
/// <param name="isRemote">Whether the file is remote.</param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="System.DateTime"/> set to the last write time of the file.
|
||||
|
/// </returns>
|
||||
|
internal /*async*/ Task<DateTime> SetCachedLastWriteTimeAsync(string imagePath, string cachedImagePath, bool isRemote) |
||||
|
{ |
||||
|
return this.SetCachedLastWriteTimeAsyncTask(imagePath, cachedImagePath, isRemote).ToTask<DateTime>(); |
||||
|
} |
||||
|
|
||||
|
#endregion
|
||||
|
|
||||
|
#region Private
|
||||
|
/// <summary>
|
||||
|
/// The create directories async tasks.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="IEnumerable{Task}"/>.
|
||||
|
/// </returns>
|
||||
|
private IEnumerable<Task> CreateDirectoriesAsyncTasks() |
||||
|
{ |
||||
|
bool success = true; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
Parallel.ForEach( |
||||
|
ValidSubDirectoryChars.ToCharArray(), |
||||
|
(extension, loop) => |
||||
|
{ |
||||
|
string path = Path.Combine(AbsoluteCachePath, extension.ToString(CultureInfo.InvariantCulture)); |
||||
|
DirectoryInfo directoryInfo = new DirectoryInfo(path); |
||||
|
|
||||
|
if (!directoryInfo.Exists) |
||||
|
{ |
||||
|
directoryInfo.Create(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
success = false; |
||||
|
} |
||||
|
|
||||
|
yield return TaskEx.FromResult(success); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds an image to the cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cachedPath">
|
||||
|
/// The cached path.
|
||||
|
/// </param>
|
||||
|
/// <param name="lastWriteTimeUtc">
|
||||
|
/// The last write time.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="IEnumerable{Task}"/>.
|
||||
|
/// </returns>
|
||||
|
private IEnumerable<Task> AddImageToCacheAsyncTask(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); |
||||
|
|
||||
|
yield break; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a value indicating whether the original file has been updated.
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">The original image path.</param>
|
||||
|
/// <param name="cachedImagePath">The cached image path.</param>
|
||||
|
/// <param name="isRemote">Whether the file is a remote request.</param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="IEnumerable{Task}"/>.
|
||||
|
/// </returns>
|
||||
|
private IEnumerable<Task> IsUpdatedFileAsyncTask(string imagePath, string cachedImagePath, bool isRemote) |
||||
|
{ |
||||
|
string key = Path.GetFileNameWithoutExtension(cachedImagePath); |
||||
|
CachedImage cachedImage; |
||||
|
bool isUpdated = false; |
||||
|
|
||||
|
if (isRemote) |
||||
|
{ |
||||
|
if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage)) |
||||
|
{ |
||||
|
// Can't check the last write time so check to see if the cached image is set to expire
|
||||
|
// or if the max age is different.
|
||||
|
if (cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration) |
||||
|
|| cachedImage.MaxAge != MaxFileCachedDuration) |
||||
|
{ |
||||
|
if (PersistantDictionary.Instance.TryRemove(key, out cachedImage)) |
||||
|
{ |
||||
|
isUpdated = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Nothing in the cache so we should return true.
|
||||
|
isUpdated = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Test now for locally requested files.
|
||||
|
if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage)) |
||||
|
{ |
||||
|
FileInfo imageFileInfo = new FileInfo(imagePath); |
||||
|
|
||||
|
if (imageFileInfo.Exists) |
||||
|
{ |
||||
|
// 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Nothing in the cache so we should return true.
|
||||
|
isUpdated = true; |
||||
|
} |
||||
|
|
||||
|
yield return TaskEx.FromResult(isUpdated); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the LastWriteTime of the cached file to match the original file.
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">
|
||||
|
/// The original image path.
|
||||
|
/// </param>
|
||||
|
/// <param name="cachedImagePath">
|
||||
|
/// The cached image path.
|
||||
|
/// </param>
|
||||
|
/// <param name="isRemote">Whether the file is remote.</param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="IEnumerable{Task}"/>.
|
||||
|
/// </returns>
|
||||
|
private IEnumerable<Task> SetCachedLastWriteTimeAsyncTask(string imagePath, string cachedImagePath, bool isRemote) |
||||
|
{ |
||||
|
FileInfo cachedFileInfo = new FileInfo(cachedImagePath); |
||||
|
DateTime lastWriteTime = DateTime.MinValue.ToUniversalTime(); |
||||
|
|
||||
|
if (isRemote) |
||||
|
{ |
||||
|
if (cachedFileInfo.Exists) |
||||
|
{ |
||||
|
lastWriteTime = cachedFileInfo.LastWriteTimeUtc; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
FileInfo imageFileInfo = new FileInfo(imagePath); |
||||
|
|
||||
|
if (imageFileInfo.Exists && cachedFileInfo.Exists) |
||||
|
{ |
||||
|
DateTime dateTime = imageFileInfo.LastWriteTimeUtc; |
||||
|
cachedFileInfo.LastWriteTimeUtc = dateTime; |
||||
|
|
||||
|
lastWriteTime = dateTime; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
yield return TaskEx.FromResult(lastWriteTime); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the correct file extension for the given string input
|
||||
|
/// </summary>
|
||||
|
/// <param name="input">
|
||||
|
/// The string to parse.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The correct file extension for the given string input if it can find one; otherwise an empty string.
|
||||
|
/// </returns>
|
||||
|
private string ParseExtension(string input) |
||||
|
{ |
||||
|
Match match = FormatRegex.Match(input); |
||||
|
|
||||
|
return match.Success ? match.Value : string.Empty; |
||||
|
} |
||||
|
#endregion
|
||||
|
#endregion
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,346 @@ |
|||||
|
// -----------------------------------------------------------------------
|
||||
|
// <copyright file="DiskCache.cs" company="James South">
|
||||
|
// Copyright (c) James South.
|
||||
|
// Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
|
// </copyright>
|
||||
|
// -----------------------------------------------------------------------
|
||||
|
|
||||
|
namespace ImageProcessor.Web.Caching |
||||
|
{ |
||||
|
#region Using
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Web; |
||||
|
using System.Web.Hosting; |
||||
|
using ImageProcessor.Helpers.Extensions; |
||||
|
using ImageProcessor.Web.Config; |
||||
|
#endregion
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encapsulates methods to handle disk caching of images.
|
||||
|
/// </summary>
|
||||
|
internal sealed class DiskCache |
||||
|
{ |
||||
|
#region Fields
|
||||
|
/// <summary>
|
||||
|
/// The maximum number of days to cache files on the system for.
|
||||
|
/// </summary>
|
||||
|
internal static readonly int MaxFileCachedDuration = ImageProcessorConfig.Instance.MaxCacheDays; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The valid sub directory chars. This used in combination with the file limit per folder
|
||||
|
/// allows the storage of 360,000 image files in the cache.
|
||||
|
/// </summary>
|
||||
|
private const string ValidSubDirectoryChars = "abcdefghijklmnopqrstuvwxyz0123456789"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum number of files allowed in the directory.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// 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.
|
||||
|
/// <see cref="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
|
||||
|
/// <see cref="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
|
||||
|
/// <see cref="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
|
||||
|
/// </remarks>
|
||||
|
private const int MaxFilesCount = 10000; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The regular expression to search strings for file extensions.
|
||||
|
/// </summary>
|
||||
|
private static readonly Regex FormatRegex = new Regex( |
||||
|
@"(jpeg|png|bmp|gif)", RegexOptions.RightToLeft | RegexOptions.Compiled); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The regular expression to search strings for valid subfolder names.
|
||||
|
/// We're specifically not using a shorter regex as we need to be able to iterate through
|
||||
|
/// each match group.
|
||||
|
/// </summary>
|
||||
|
private static readonly Regex SubFolderRegex = new Regex(@"(\/(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|0|1|2|3|4|5|6|7|8|9)\/)", RegexOptions.Compiled); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The absolute path to virtual cache path on the server.
|
||||
|
/// </summary>
|
||||
|
private static readonly string AbsoluteCachePath = |
||||
|
HostingEnvironment.MapPath(ImageProcessorConfig.Instance.VirtualCachePath); |
||||
|
|
||||
|
#endregion
|
||||
|
|
||||
|
#region Methods
|
||||
|
/// <summary>
|
||||
|
/// The create cache paths.
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// The true if the cache directories are created successfully; otherwise, false.
|
||||
|
/// </returns>
|
||||
|
internal static bool CreateCacheDirectories() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
Parallel.ForEach( |
||||
|
ValidSubDirectoryChars.ToCharArray(), |
||||
|
(extension, loop) => |
||||
|
{ |
||||
|
string path = Path.Combine(AbsoluteCachePath, extension.ToString(CultureInfo.InvariantCulture)); |
||||
|
DirectoryInfo directoryInfo = new DirectoryInfo(path); |
||||
|
|
||||
|
if (!directoryInfo.Exists) |
||||
|
{ |
||||
|
directoryInfo.Create(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the full transformed cached path for the image.
|
||||
|
/// The file names are stored as MD5 encrypted versions of the full request path.
|
||||
|
/// This should make them unique enough to
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">The original image path.</param>
|
||||
|
/// <param name="imageName">The original image name.</param>
|
||||
|
/// <returns>The full cached path for the image.</returns>
|
||||
|
internal static string GetCachePath(string imagePath, string imageName) |
||||
|
{ |
||||
|
string cachedPath = string.Empty; |
||||
|
|
||||
|
if (AbsoluteCachePath != null) |
||||
|
{ |
||||
|
// Use an md5 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 first character of that hash as a subfolder.
|
||||
|
string parsedExtension = ParseExtension(imagePath); |
||||
|
string fallbackExtension = imageName.Substring(imageName.LastIndexOf(".", StringComparison.Ordinal) + 1); |
||||
|
string encryptedName = imagePath.ToMD5Fingerprint(); |
||||
|
string subpath = encryptedName.Substring(0, 1); |
||||
|
|
||||
|
string cachedFileName = string.Format( |
||||
|
"{0}.{1}", |
||||
|
encryptedName, |
||||
|
!string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension : fallbackExtension); |
||||
|
|
||||
|
cachedPath = Path.Combine(AbsoluteCachePath, subpath, cachedFileName); |
||||
|
} |
||||
|
|
||||
|
return cachedPath; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds an image to the cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cachedPath">
|
||||
|
/// The cached path.
|
||||
|
/// </param>
|
||||
|
/// <param name="lastWriteTimeUtc">
|
||||
|
/// The last write time.
|
||||
|
/// </param>
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts an absolute file path
|
||||
|
/// </summary>
|
||||
|
/// <param name="absolutePath">The absolute path to convert.</param>
|
||||
|
/// <param name="request">The <see cref="T:System.Web.HttpRequest"/>from the current context.</param>
|
||||
|
/// <returns>The virtual path to the file.</returns>
|
||||
|
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."); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a value indicating whether the original file has been updated.
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">The original image path.</param>
|
||||
|
/// <param name="cachedImagePath">The cached image path.</param>
|
||||
|
/// <param name="isRemote">Whether the file is a remote request.</param>
|
||||
|
/// <returns>
|
||||
|
/// True if the the original file has been updated; otherwise, false.
|
||||
|
/// </returns>
|
||||
|
internal static bool IsUpdatedFile(string imagePath, string cachedImagePath, bool isRemote) |
||||
|
{ |
||||
|
string key = Path.GetFileNameWithoutExtension(cachedImagePath); |
||||
|
CachedImage cachedImage; |
||||
|
|
||||
|
if (isRemote) |
||||
|
{ |
||||
|
if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage)) |
||||
|
{ |
||||
|
// Can't check the last write time so check to see if the cached image is set to expire
|
||||
|
// or if the max age is different.
|
||||
|
if (cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration) |
||||
|
|| cachedImage.MaxAge != MaxFileCachedDuration) |
||||
|
{ |
||||
|
if (PersistantDictionary.Instance.TryRemove(key, out cachedImage)) |
||||
|
{ |
||||
|
// We can jump out here.
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Nothing in the cache so we should return true.
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Test now for locally requested files.
|
||||
|
if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage)) |
||||
|
{ |
||||
|
FileInfo imageFileInfo = new FileInfo(imagePath); |
||||
|
|
||||
|
if (imageFileInfo.Exists) |
||||
|
{ |
||||
|
// 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)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Nothing in the cache so we should return true.
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the LastWriteTime of the cached file to match the original file.
|
||||
|
/// </summary>
|
||||
|
/// <param name="imagePath">
|
||||
|
/// The original image path.
|
||||
|
/// </param>
|
||||
|
/// <param name="cachedImagePath">
|
||||
|
/// The cached image path.
|
||||
|
/// </param>
|
||||
|
/// <param name="isRemote">Whether the file is remote.</param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="System.DateTime"/> set to the last write time of the file.
|
||||
|
/// </returns>
|
||||
|
internal static DateTime SetCachedLastWriteTime(string imagePath, string cachedImagePath, bool isRemote) |
||||
|
{ |
||||
|
FileInfo cachedFileInfo = new FileInfo(cachedImagePath); |
||||
|
|
||||
|
if (isRemote) |
||||
|
{ |
||||
|
if (cachedFileInfo.Exists) |
||||
|
{ |
||||
|
return cachedFileInfo.LastWriteTimeUtc; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
FileInfo imageFileInfo = new FileInfo(imagePath); |
||||
|
|
||||
|
if (imageFileInfo.Exists && cachedFileInfo.Exists) |
||||
|
{ |
||||
|
DateTime dateTime = imageFileInfo.LastWriteTimeUtc; |
||||
|
cachedFileInfo.LastWriteTimeUtc = dateTime; |
||||
|
|
||||
|
return dateTime; |
||||
|
} |
||||
|
|
||||
|
return DateTime.MinValue.ToUniversalTime(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Purges any files from the file-system cache in the given folders.
|
||||
|
/// </summary>
|
||||
|
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 => SubFolderRegex.Match(x.Value.Path).Value) |
||||
|
.Where(g => g.Count() > MaxFilesCount); |
||||
|
|
||||
|
foreach (var group in groups) |
||||
|
{ |
||||
|
int groupCount = group.Count(); |
||||
|
|
||||
|
foreach (KeyValuePair<string, CachedImage> 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the correct file extension for the given string input
|
||||
|
/// </summary>
|
||||
|
/// <param name="input">
|
||||
|
/// The string to parse.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The correct file extension for the given string input if it can find one; otherwise an empty string.
|
||||
|
/// </returns>
|
||||
|
private static string ParseExtension(string input) |
||||
|
{ |
||||
|
Match match = FormatRegex.Match(input); |
||||
|
|
||||
|
return match.Success ? match.Value : string.Empty; |
||||
|
} |
||||
|
|
||||
|
#endregion
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,174 @@ |
|||||
|
// License: CPOL at http://www.codeproject.com/info/cpol10.aspx
|
||||
|
|
||||
|
|
||||
|
namespace System.Threading.Tasks |
||||
|
{ |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Extensions related to the <see cref="Task"/> classes.
|
||||
|
/// Supports implementing "async"-style methods in C#4 using iterators.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// I would call this TaskExtensions, except that clients must name the class to use methods like <see cref="FromResult{T}(T)"/>.
|
||||
|
/// Based on work from Await Tasks in C#4 using Iterators by Keith L Robertson.
|
||||
|
/// <see cref="http://www.codeproject.com/Articles/504197/Await-Tasks-in-Csharp4-using-Iterators"/>
|
||||
|
/// </remarks>
|
||||
|
public static class TaskEx |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Return a Completed <see cref="Task{TResult}"/> with a specific <see cref="Task{TResult}.Result"/> value.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">
|
||||
|
/// The result
|
||||
|
/// </typeparam>
|
||||
|
/// <param name="resultValue">
|
||||
|
/// The result Value.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="Task"/>.
|
||||
|
/// </returns>
|
||||
|
public static Task<TResult> FromResult<TResult>(TResult resultValue) |
||||
|
{ |
||||
|
var completionSource = new TaskCompletionSource<TResult>(); |
||||
|
completionSource.SetResult(resultValue); |
||||
|
return completionSource.Task; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Transform an enumeration of <see cref="Task"/> into a single non-Result <see cref="Task"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="tasks">
|
||||
|
/// The tasks.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="Task"/>.
|
||||
|
/// </returns>
|
||||
|
public static Task ToTask(this IEnumerable<Task> tasks) |
||||
|
{ |
||||
|
return ToTask<VoidResult>(tasks); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Transform an enumeration of <see cref="Task"/> into a single <see cref="Task{TResult}"/>.
|
||||
|
/// The final <see cref="Task"/> in <paramref name="tasks"/> must be a <see cref="Task{TResult}"/>.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TResult">
|
||||
|
/// The task results
|
||||
|
/// </typeparam>
|
||||
|
/// <param name="tasks">
|
||||
|
/// The tasks.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="Task"/>.
|
||||
|
/// </returns>
|
||||
|
public static Task<TResult> ToTask<TResult>(this IEnumerable<Task> tasks) |
||||
|
{ |
||||
|
var taskScheduler = |
||||
|
SynchronizationContext.Current == null |
||||
|
? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext(); |
||||
|
var taskEnumerator = tasks.GetEnumerator(); |
||||
|
var completionSource = new TaskCompletionSource<TResult>(); |
||||
|
|
||||
|
// Clean up the enumerator when the task completes.
|
||||
|
completionSource.Task.ContinueWith(t => taskEnumerator.Dispose(), taskScheduler); |
||||
|
|
||||
|
ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, null); |
||||
|
return completionSource.Task; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// If the previous task Canceled or Faulted, complete the master task with the same <see cref="Task.Status"/>.
|
||||
|
/// Obtain the next <see cref="Task"/> from the <paramref name="taskEnumerator"/>.
|
||||
|
/// If none, complete the master task, possibly with the <see cref="Task{T}.Result"/> of the last task.
|
||||
|
/// Otherwise, set up the task with a continuation to come do this again when it completes.
|
||||
|
/// </summary>
|
||||
|
private static void ToTaskDoOneStep<TResult>( |
||||
|
IEnumerator<Task> taskEnumerator, TaskScheduler taskScheduler, |
||||
|
TaskCompletionSource<TResult> completionSource, Task completedTask) |
||||
|
{ |
||||
|
// Check status of previous nested task (if any), and stop if Canceled or Faulted.
|
||||
|
TaskStatus status; |
||||
|
if (completedTask == null) |
||||
|
{ |
||||
|
// This is the first task from the iterator; skip status check.
|
||||
|
} |
||||
|
else if ((status = completedTask.Status) == TaskStatus.Canceled) |
||||
|
{ |
||||
|
completionSource.SetCanceled(); |
||||
|
return; |
||||
|
} |
||||
|
else if (status == TaskStatus.Faulted) |
||||
|
{ |
||||
|
completionSource.SetException(completedTask.Exception); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Check for cancellation before looking for the next task.
|
||||
|
// This causes a problem where the ultimate Task does not complete and fire any continuations; I don't know why.
|
||||
|
// So cancellation from the Token must be handled within the iterator itself.
|
||||
|
//if (cancellationToken.IsCancellationRequested) {
|
||||
|
// completionSource.SetCanceled();
|
||||
|
// return;
|
||||
|
//}
|
||||
|
|
||||
|
// Find the next Task in the iterator; handle cancellation and other exceptions.
|
||||
|
Boolean haveMore; |
||||
|
try |
||||
|
{ |
||||
|
haveMore = taskEnumerator.MoveNext(); |
||||
|
|
||||
|
} |
||||
|
catch (OperationCanceledException cancExc) |
||||
|
{ |
||||
|
//if (cancExc.CancellationToken == cancellationToken) completionSource.SetCanceled();
|
||||
|
//else completionSource.SetException(cancExc);
|
||||
|
completionSource.SetCanceled(); |
||||
|
return; |
||||
|
} |
||||
|
catch (Exception exc) |
||||
|
{ |
||||
|
completionSource.SetException(exc); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!haveMore) |
||||
|
{ |
||||
|
// No more tasks; set the result from the last completed task (if any, unless no result is requested).
|
||||
|
// We know it's not Canceled or Faulted because we checked at the start of this method.
|
||||
|
if (typeof(TResult) == typeof(VoidResult)) |
||||
|
{ // No result
|
||||
|
completionSource.SetResult(default(TResult)); |
||||
|
} |
||||
|
else if (!(completedTask is Task<TResult>)) |
||||
|
{ // Wrong result
|
||||
|
completionSource.SetException(new InvalidOperationException( |
||||
|
"Asynchronous iterator " + taskEnumerator + |
||||
|
" requires a final result task of type " + typeof(Task<TResult>).FullName + |
||||
|
(completedTask == null ? ", but none was provided." : |
||||
|
"; the actual task type was " + completedTask.GetType().FullName))); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
completionSource.SetResult(((Task<TResult>)completedTask).Result); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// When the nested task completes, continue by performing this function again.
|
||||
|
// Note: This is NOT a recursive call; the current method activation will complete
|
||||
|
// almost immediately and independently of the lambda continuation.
|
||||
|
taskEnumerator.Current.ContinueWith( |
||||
|
nextTask => ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, nextTask), |
||||
|
taskScheduler); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Internal marker type for using <see cref="ToTask{T}"/> to implement <see cref="ToTask"/>.
|
||||
|
/// </summary>
|
||||
|
private abstract class VoidResult |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue