Browse Source

Better remote error handling + cleanup

Former-commit-id: 67cb999f6a10932131f5f4a723587a45bae2904d
af/merge-core
James South 12 years ago
parent
commit
e92ebb8b8f
  1. 143
      src/ImageProcessor.Web/Caching/DiskCache.cs
  2. 47
      src/ImageProcessor.Web/Helpers/RemoteFile.cs
  3. 13
      src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs
  4. 4
      src/ImageProcessor/Configuration/NativeMethods.cs
  5. 2
      src/TestWebsites/MVC/Web.config

143
src/ImageProcessor.Web/Caching/DiskCache.cs

@ -16,7 +16,6 @@ namespace ImageProcessor.Web.Caching
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Web.Hosting; using System.Web.Hosting;
using ImageProcessor.Web.Configuration; using ImageProcessor.Web.Configuration;
@ -130,14 +129,65 @@ namespace ImageProcessor.Web.Caching
} }
#region Methods #region Methods
#region Internal #region Public
/// <summary>
/// Trims a cached folder ensuring that it does not exceed the maximum file count.
/// </summary>
/// <param name="path">
/// The path to the folder.
/// </param>
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<FileInfo> 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.
}
}
}
}
}
}
/// <summary> /// <summary>
/// Adds an image to the cache. /// Adds an image to the cache.
/// </summary> /// </summary>
/// <param name="cachedPath"> /// <param name="cachedPath">
/// The path to the cached image. /// The path to the cached image.
/// </param> /// </param>
internal void AddImageToCache(string cachedPath) public void AddImageToCache(string cachedPath)
{ {
string key = Path.GetFileNameWithoutExtension(cachedPath); string key = Path.GetFileNameWithoutExtension(cachedPath);
CachedImage cachedImage = new CachedImage CachedImage cachedImage = new CachedImage
@ -159,7 +209,7 @@ namespace ImageProcessor.Web.Caching
/// <returns> /// <returns>
/// True if the the original file is new or has been updated; otherwise, false. /// True if the the original file is new or has been updated; otherwise, false.
/// </returns> /// </returns>
internal bool IsNewOrUpdatedFile(string cachedPath) public bool IsNewOrUpdatedFile(string cachedPath)
{ {
bool isUpdated = false; bool isUpdated = false;
CachedImage cachedImage = CacheIndexer.GetValue(cachedPath); CachedImage cachedImage = CacheIndexer.GetValue(cachedPath);
@ -172,7 +222,7 @@ namespace ImageProcessor.Web.Caching
else else
{ {
// Check to see if the cached image is set to expire. // Check to see if the cached image is set to expire.
if (this.IsExpired(cachedImage.CreationTimeUtc)) if (IsExpired(cachedImage.CreationTimeUtc))
{ {
CacheIndexer.Remove(cachedPath); CacheIndexer.Remove(cachedPath);
isUpdated = true; isUpdated = true;
@ -181,72 +231,22 @@ namespace ImageProcessor.Web.Caching
return isUpdated; return isUpdated;
} }
/// <summary>
/// Trims a cached folder ensuring that it does not exceed the maximum file count.
/// </summary>
/// <param name="path">
/// The path to the folder.
/// </param>
/// <returns>
/// The <see cref="T:System.Threading.Tasks.Task"/>.
/// </returns>
internal async Task TrimCachedFolderAsync(string path)
{
await Task.Run(() => this.TrimCachedFolders(path));
}
#endregion #endregion
#region Private #region Private
/// <summary> /// <summary>
/// Trims a cached folder ensuring that it does not exceed the maximum file count. /// Gets a value indicating whether the given images creation date is out with
/// the prescribed limit.
/// </summary> /// </summary>
/// <param name="path"> /// <param name="creationDate">
/// The path to the folder. /// The creation date.
/// </param> /// </param>
private void TrimCachedFolders(string path) /// <returns>
/// The true if the date is out with the limit, otherwise; false.
/// </returns>
private static bool IsExpired(DateTime creationDate)
{ {
string directory = Path.GetDirectoryName(path); return creationDate.AddDays(MaxFileCachedDuration) < DateTime.UtcNow.AddDays(-MaxFileCachedDuration);
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<FileInfo> 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.
}
}
}
}
}
} }
/// <summary> /// <summary>
@ -305,21 +305,6 @@ namespace ImageProcessor.Web.Caching
this.virtualCachedPath = Path.Combine(VirtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/"); this.virtualCachedPath = Path.Combine(VirtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/");
} }
} }
/// <summary>
/// Gets a value indicating whether the given images creation date is out with
/// the prescribed limit.
/// </summary>
/// <param name="creationDate">
/// The creation date.
/// </param>
/// <returns>
/// The true if the date is out with the limit, otherwise; false.
/// </returns>
private bool IsExpired(DateTime creationDate)
{
return creationDate.AddDays(MaxFileCachedDuration) < DateTime.UtcNow.AddDays(-MaxFileCachedDuration);
}
#endregion #endregion
#endregion #endregion
} }

47
src/ImageProcessor.Web/Helpers/RemoteFile.cs

@ -13,12 +13,15 @@ namespace ImageProcessor.Web.Helpers
#region Using #region Using
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Security; using System.Security;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Web.Configuration; using ImageProcessor.Web.Configuration;
#endregion #endregion
@ -241,27 +244,43 @@ namespace ImageProcessor.Web.Helpers
/// </returns> /// </returns>
internal async Task<WebResponse> GetWebResponseAsync() internal async Task<WebResponse> GetWebResponseAsync()
{ {
WebResponse response = await this.GetWebRequest().GetResponseAsync(); WebResponse response = null;
try
{
response = await this.GetWebRequest().GetResponseAsync();
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.NameResolutionFailure)
{
throw new HttpException(404, "No image exists at " + Uri);
}
long contentLength = response.ContentLength; throw;
}
// WebResponse.ContentLength doesn't always know the value, it returns -1 in this case. if (response != null)
if (contentLength == -1)
{ {
// Response headers may still have the Content-Length inside of it. long contentLength = response.ContentLength;
string headerContentLength = response.Headers["Content-Length"];
if (!string.IsNullOrWhiteSpace(headerContentLength)) // WebResponse.ContentLength doesn't always know the value, it returns -1 in this case.
if (contentLength == -1)
{ {
contentLength = long.Parse(headerContentLength, CultureInfo.InvariantCulture); // Response headers may still have the Content-Length inside of it.
string headerContentLength = response.Headers["Content-Length"];
if (!string.IsNullOrWhiteSpace(headerContentLength))
{
contentLength = long.Parse(headerContentLength, CultureInfo.InvariantCulture);
}
} }
}
// We don't need to check the url here since any external urls are available only from the web.config. // We don't need to check the url here since any external urls are available only from the web.config.
if ((this.MaxDownloadSize > 0) && (contentLength > this.MaxDownloadSize)) if ((this.MaxDownloadSize > 0) && (contentLength > this.MaxDownloadSize))
{ {
response.Close(); response.Close();
throw new SecurityException("An attempt to download a remote file has been halted because the file is larger than allowed."); throw new SecurityException("An attempt to download a remote file has been halted because the file is larger than allowed.");
}
} }
return response; return response;

13
src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs

@ -225,6 +225,9 @@ namespace ImageProcessor.Web.HttpModules
{ {
string cachedPath = cachedPathObject.ToString(); string cachedPath = cachedPathObject.ToString();
// Trim the cache.
DiskCache.TrimCachedFolders(cachedPath);
// Fire the post processing event. // Fire the post processing event.
EventHandler<PostProcessingEventArgs> handler = OnPostProcessing; EventHandler<PostProcessingEventArgs> handler = OnPostProcessing;
if (handler != null) if (handler != null)
@ -347,7 +350,7 @@ namespace ImageProcessor.Web.HttpModules
{ {
string hashedUrlParameters = urlParameters.ToMD5Fingerprint(); string hashedUrlParameters = urlParameters.ToMD5Fingerprint();
// TODO: Add hash for querystring parameters. // TODO: Add hash for querystring parameters?
imageName += hashedUrlParameters; imageName += hashedUrlParameters;
fullPath += hashedUrlParameters; fullPath += hashedUrlParameters;
} }
@ -425,9 +428,6 @@ namespace ImageProcessor.Web.HttpModules
// Add to the cache. // Add to the cache.
cache.AddImageToCache(cachedPath); cache.AddImageToCache(cachedPath);
// Trim the cache.
await cache.TrimCachedFolderAsync(cachedPath);
// Store the cached path, response type, and cache dependency in the context for later retrieval. // Store the cached path, response type, and cache dependency in the context for later retrieval.
context.Items[CachedPathKey] = cachedPath; context.Items[CachedPathKey] = cachedPath;
context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType; context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType;
@ -440,7 +440,7 @@ namespace ImageProcessor.Web.HttpModules
} }
else else
{ {
using (await Locker.LockAsync(cachedPath)) using (Locker.Lock(cachedPath))
{ {
// Check to see if the file exists. // Check to see if the file exists.
// ReSharper disable once AssignNullToNotNullAttribute // ReSharper disable once AssignNullToNotNullAttribute
@ -459,9 +459,6 @@ namespace ImageProcessor.Web.HttpModules
// Add to the cache. // Add to the cache.
cache.AddImageToCache(cachedPath); cache.AddImageToCache(cachedPath);
// Trim the cache.
await cache.TrimCachedFolderAsync(cachedPath);
// Store the cached path, response type, and cache dependencies in the context for later retrieval. // Store the cached path, response type, and cache dependencies in the context for later retrieval.
context.Items[CachedPathKey] = cachedPath; context.Items[CachedPathKey] = cachedPath;
context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType; context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType;

4
src/ImageProcessor/Configuration/NativeMethods.cs

@ -27,7 +27,7 @@ namespace ImageProcessor.Configuration
/// an executable module. /// an executable module.
/// </param> /// </param>
/// <returns>If the function succeeds, the return value is a handle to the module; otherwise null.</returns> /// <returns>If the function succeeds, the return value is a handle to the module; otherwise null.</returns>
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)] [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr LoadLibrary(string libname); public static extern IntPtr LoadLibrary(string libname);
/// <summary> /// <summary>
@ -38,7 +38,7 @@ namespace ImageProcessor.Configuration
/// <param name="hModule">A handle to the loaded library module. /// <param name="hModule">A handle to the loaded library module.
/// The LoadLibrary, LoadLibraryEx, GetModuleHandle, or GetModuleHandleEx function returns this handle.</param> /// The LoadLibrary, LoadLibraryEx, GetModuleHandle, or GetModuleHandleEx function returns this handle.</param>
/// <returns>If the function succeeds, the return value is nonzero; otherwise zero.</returns> /// <returns>If the function succeeds, the return value is nonzero; otherwise zero.</returns>
[DllImport("kernel32", CharSet = CharSet.Auto)] [DllImport("kernel32", SetLastError = true)]
public static extern bool FreeLibrary(IntPtr hModule); public static extern bool FreeLibrary(IntPtr hModule);
/// <summary> /// <summary>

2
src/TestWebsites/MVC/Web.config

@ -28,7 +28,7 @@
<system.web> <system.web>
<httpRuntime targetFramework="4.5" /> <httpRuntime targetFramework="4.5" />
<customErrors mode="Off"/> <!--<customErrors mode="Off"/>-->
<compilation debug="true" targetFramework="4.5" /> <compilation debug="true" targetFramework="4.5" />
<pages> <pages>

Loading…
Cancel
Save