diff --git a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs new file mode 100644 index 000000000..4706fd857 --- /dev/null +++ b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs @@ -0,0 +1,98 @@ +namespace ImageProcessor.Web.AzureBlobCache +{ + using System; + using System.Configuration; + using System.Threading.Tasks; + using System.Web; + + using ImageProcessor.Web.Caching; + + using Microsoft.WindowsAzure; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.Blob; + + public class AzureBlobCache : ImageCacheBase + { + private CloudStorageAccount cloudStorageAccount; + + private CloudBlobClient cloudBlobClient; + + private CloudBlobContainer cloudBlobContainer; + + public AzureBlobCache(string requestPath, string fullPath, string querystring) + : base(requestPath, fullPath, querystring) + { + // TODO: These should all be in the configuration. + + // Retrieve storage account from connection string. + this.cloudStorageAccount = CloudStorageAccount.Parse( + CloudConfigurationManager.GetSetting("StorageConnectionString")); + + // Create the blob client. + this.cloudBlobClient = this.cloudStorageAccount.CreateCloudBlobClient(); + + // Retrieve reference to a previously created container. + this.cloudBlobContainer = this.cloudBlobClient.GetContainerReference("mycontainer"); + } + + public override int MaxAge + { + get { throw new System.NotImplementedException(); } + } + + public override async Task IsNewOrUpdatedAsync() + { + string cachedFileName = await this.CreateCachedFileName(); + + // TODO: Generate cache path. + CloudBlockBlob blockBlob = new CloudBlockBlob(new Uri("")); + + bool isUpdated = false; + if (!await blockBlob.ExistsAsync()) + { + // Nothing in the cache so we should return true. + isUpdated = true; + } + else if (blockBlob.Properties.LastModified.HasValue) + { + // Check to see if the cached image is set to expire. + if (this.IsExpired(blockBlob.Properties.LastModified.Value.UtcDateTime)) + { + isUpdated = true; + } + } + + return isUpdated; + } + + public override async Task AddImageToCacheAsync(System.IO.Stream stream) + { + throw new System.NotImplementedException(); + } + + public override async Task TrimCacheAsync() + { + throw new System.NotImplementedException(); + } + + public override void RewritePath(HttpContext context) + { + throw new System.NotImplementedException(); + } + + /// + /// Gets a value indicating whether the given images creation date is out with + /// the prescribed limit. + /// + /// + /// The creation date. + /// + /// + /// The true if the date is out with the limit, otherwise; false. + /// + private bool IsExpired(DateTime creationDate) + { + return creationDate.AddDays(this.MaxAge) < DateTime.UtcNow.AddDays(-this.MaxAge); + } + } +} diff --git a/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj b/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj new file mode 100644 index 000000000..4d30e737d --- /dev/null +++ b/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj @@ -0,0 +1,95 @@ + + + + + Debug + AnyCPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7} + Library + Properties + ImageProcessor.Web.AzureBlobCache + ImageProcessor.Web.AzureBlobCache + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.Data.Edm.5.6.2\lib\net40\Microsoft.Data.Edm.dll + True + + + ..\packages\Microsoft.Data.OData.5.6.2\lib\net40\Microsoft.Data.OData.dll + True + + + False + ..\packages\Microsoft.Data.Services.Client.5.6.2\lib\net40\Microsoft.Data.Services.Client.dll + + + ..\packages\Microsoft.WindowsAzure.ConfigurationManager.1.8.0.0\lib\net35-full\Microsoft.WindowsAzure.Configuration.dll + + + ..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll + True + + + + + False + ..\packages\System.Spatial.5.6.2\lib\net40\System.Spatial.dll + + + + + + + + + + + + + + + {d011a778-59c8-4bfa-a770-c350216bf161} + ImageProcessor.Web + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/ImageProcessor.Web.AzureBlobCache/Properties/AssemblyInfo.cs b/src/ImageProcessor.Web.AzureBlobCache/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..33c5979f9 --- /dev/null +++ b/src/ImageProcessor.Web.AzureBlobCache/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ImageProcessor.Web.AzureBlobCache")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ImageProcessor.Web.AzureBlobCache")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("90605e94-25f4-4c69-b602-6b1df0adb89e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/ImageProcessor.Web.AzureBlobCache/packages.config b/src/ImageProcessor.Web.AzureBlobCache/packages.config new file mode 100644 index 000000000..5cdca6448 --- /dev/null +++ b/src/ImageProcessor.Web.AzureBlobCache/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/ImageProcessor.Web/Caching/DiskCache2.cs b/src/ImageProcessor.Web/Caching/DiskCache2.cs new file mode 100644 index 000000000..2ea9e34a6 --- /dev/null +++ b/src/ImageProcessor.Web/Caching/DiskCache2.cs @@ -0,0 +1,180 @@ +namespace ImageProcessor.Web.Caching +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using System.Web; + using System.Web.Hosting; + + using ImageProcessor.Web.Configuration; + using ImageProcessor.Web.Extensions; + + public class DiskCache2 : 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 virtual cache path. + /// + private static readonly string VirtualCachePath = ImageProcessorConfiguration.Instance.VirtualCachePath; + + /// + /// The absolute path to virtual cache path on the server. + /// TODO: Change this so configuration is determined per IImageCache instance. + /// + private static readonly string AbsoluteCachePath = HostingEnvironment.MapPath(VirtualCachePath); + + /// + /// The physical cached path. + /// + private string physicalCachedPath; + + /// + /// The virtual cached path. + /// + private string virtualCachedPath; + + public DiskCache2(string requestPath, string fullPath, string querystring) + : base(requestPath, fullPath, querystring) + { + } + + /// + /// The maximum number of days to cache files on the system for. + /// TODO: Shift the getter source to proper config. + /// + public override int MaxAge + { + get + { + return ImageProcessorConfiguration.Instance.MaxCacheDays; + } + } + + 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 for files. + string pathFromKey = string.Join("\\", cachedFileName.ToCharArray().Take(6)); + string virtualPathFromKey = pathFromKey.Replace(@"\", "/"); + this.physicalCachedPath = Path.Combine(AbsoluteCachePath, pathFromKey, cachedFileName); + this.virtualCachedPath = Path.Combine(VirtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/"); + this.CachedPath = this.physicalCachedPath; + + bool isUpdated = false; + CachedImage cachedImage = CacheIndexer.GetValue(this.physicalCachedPath); + + 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.physicalCachedPath); + isUpdated = true; + } + } + + return isUpdated; + } + + public override async Task AddImageToCacheAsync(Stream stream) + { + // ReSharper disable once AssignNullToNotNullAttribute + DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.physicalCachedPath)); + if (!directoryInfo.Exists) + { + directoryInfo.Create(); + } + + using (FileStream fileStream = File.Create(this.physicalCachedPath)) + { + await stream.CopyToAsync(fileStream); + } + } + + public override async Task TrimCacheAsync() + { + string directory = Path.GetDirectoryName(this.physicalCachedPath); + + 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. + } + } + } + } + } + } + + public override void RewritePath(HttpContext context) + { + // The cached file is valid so just rewrite the path. + context.RewritePath(this.virtualCachedPath, false); + } + + /// + /// Gets a value indicating whether the given images creation date is out with + /// the prescribed limit. + /// + /// + /// The creation date. + /// + /// + /// The true if the date is out with the limit, otherwise; false. + /// + private bool IsExpired(DateTime creationDate) + { + return creationDate.AddDays(this.MaxAge) < DateTime.UtcNow.AddDays(-this.MaxAge); + } + } +} diff --git a/src/ImageProcessor.Web/Caching/IImageCache.cs b/src/ImageProcessor.Web/Caching/IImageCache.cs new file mode 100644 index 000000000..6153c1f8a --- /dev/null +++ b/src/ImageProcessor.Web/Caching/IImageCache.cs @@ -0,0 +1,26 @@ + +namespace ImageProcessor.Web.Caching +{ + using System.IO; + using System.Threading.Tasks; + using System.Web; + + public interface IImageCache + { + string CachedPath { get; } + + int MaxAge { get; } + + Task IsNewOrUpdatedAsync(); + + Task AddImageToCacheAsync(Stream stream); + + Task TrimCacheAsync(); + + Task CreateCachedFileName(); + + void RewritePath(HttpContext context); + + //void SetHeaders(HttpContext context); + } +} diff --git a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs new file mode 100644 index 000000000..c33a03e60 --- /dev/null +++ b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs @@ -0,0 +1,125 @@ +namespace ImageProcessor.Web.Caching +{ + using System; + using System.Globalization; + using System.IO; + using System.Reflection; + using System.Threading.Tasks; + using System.Web; + + using ImageProcessor.Web.Extensions; + using ImageProcessor.Web.Helpers; + + public abstract class ImageCacheBase : IImageCache + { + /// + /// The assembly version. + /// + private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + + /// + /// The request path for the image. + /// + private readonly string requestPath; + + /// + /// The full path for the image. + /// + private readonly string fullPath; + + /// + /// The querystring containing processing instructions. + /// + private readonly string querystring; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The request path for the image. + /// + /// + /// The full path for the image. + /// + /// + /// The querystring containing instructions. + /// + protected ImageCacheBase(string requestPath, string fullPath, string querystring) + { + this.requestPath = requestPath; + this.fullPath = fullPath; + this.querystring = querystring; + } + + public string CachedPath { get; protected set; } + + public abstract int MaxAge { get; } + + public abstract Task IsNewOrUpdatedAsync(); + + public abstract Task AddImageToCacheAsync(Stream stream); + + public abstract Task TrimCacheAsync(); + + public Task CreateCachedFileName() + { + string streamHash = string.Empty; + + try + { + if (new Uri(this.requestPath).IsFile) + { + // Get the hash for the filestream. That way we can ensure that if the image is + // updated but has the same name we will know. + FileInfo imageFileInfo = new FileInfo(this.requestPath); + if (imageFileInfo.Exists) + { + // Pull the latest info. + imageFileInfo.Refresh(); + + // Checking the stream itself is far too processor intensive so we make a best guess. + string creation = imageFileInfo.CreationTimeUtc.ToString(CultureInfo.InvariantCulture); + string length = imageFileInfo.Length.ToString(CultureInfo.InvariantCulture); + streamHash = string.Format("{0}{1}", creation, length); + } + } + } + catch + { + streamHash = string.Empty; + } + + // Use an sha1 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 characters of that hash as sub-folders. + string parsedExtension = ImageHelpers.GetExtension(this.fullPath, this.querystring); + string encryptedName = (streamHash + this.fullPath).ToSHA1Fingerprint(); + + string cachedFileName = string.Format( + "{0}.{1}", + encryptedName, + !string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension.Replace(".", string.Empty) : "jpg"); + + this.CachedPath = cachedFileName; + return Task.FromResult(cachedFileName); + } + + public abstract void RewritePath(HttpContext context); + + public virtual void SetHeaders(HttpContext context, string responseType) + { + HttpResponse response = context.Response; + + response.ContentType = responseType; + + if (response.Headers["Image-Served-By"] == null) + { + response.AddHeader("Image-Served-By", "ImageProcessor.Web/" + AssemblyVersion); + } + + HttpCachePolicy cache = response.Cache; + cache.SetCacheability(HttpCacheability.Public); + cache.VaryByHeaders["Accept-Encoding"] = true; + } + } +} diff --git a/src/ImageProcessor.Web/Extensions/DirectoryInfoExtensions.cs b/src/ImageProcessor.Web/Extensions/DirectoryInfoExtensions.cs index 2ec477257..0ad55ca60 100644 --- a/src/ImageProcessor.Web/Extensions/DirectoryInfoExtensions.cs +++ b/src/ImageProcessor.Web/Extensions/DirectoryInfoExtensions.cs @@ -13,6 +13,7 @@ namespace ImageProcessor.Web.Extensions using System.Collections.Generic; using System.IO; using System.Linq; + using System.Threading.Tasks; /// /// Provides extension methods to the type. @@ -37,7 +38,36 @@ namespace ImageProcessor.Web.Extensions /// /// An enumerable collection of directories that matches searchPattern and searchOption. /// - public static IEnumerable SafeEnumerateDirectories(this DirectoryInfo directoryInfo, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + public static Task> SafeEnumerateDirectoriesAsync( + this DirectoryInfo directoryInfo, + string searchPattern = "*", + SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + return Task.Run(() => SafeEnumerateDirectories(directoryInfo, searchPattern, searchOption)); + } + + /// + /// Returns an enumerable collection of directory information that matches a specified search pattern and search subdirectory option. + /// Will return an empty enumerable on exception. Quick and dirty but does what I need just now. + /// + /// + /// The that this method extends. + /// + /// + /// The search string to match against the names of directories. This parameter can contain a combination of valid literal path + /// and wildcard (* and ?) characters (see Remarks), but doesn't support regular expressions. The default pattern is "*", which returns all files. + /// + /// + /// One of the enumeration values that specifies whether the search operation should include only + /// the current directory or all subdirectories. The default value is TopDirectoryOnly. + /// + /// + /// An enumerable collection of directories that matches searchPattern and searchOption. + /// + public static IEnumerable SafeEnumerateDirectories( + this DirectoryInfo directoryInfo, + string searchPattern = "*", + SearchOption searchOption = SearchOption.TopDirectoryOnly) { IEnumerable directories; diff --git a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs index 4db41ee0f..5015c2ac4 100644 --- a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs +++ b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs @@ -85,6 +85,8 @@ namespace ImageProcessor.Web.HttpModules /// life in the Garbage Collector. /// private bool isDisposed; + + private IImageCache imageCache; #endregion #region Destructors @@ -222,7 +224,7 @@ namespace ImageProcessor.Web.HttpModules /// /// The . /// - private Task PostProcessImage(object sender, EventArgs e) + private async Task PostProcessImage(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; object cachedPathObject = context.Items[CachedPathKey]; @@ -232,18 +234,16 @@ namespace ImageProcessor.Web.HttpModules string cachedPath = cachedPathObject.ToString(); // Trim the cache. - DiskCache.TrimCachedFolders(cachedPath); + await this.imageCache.TrimCacheAsync(); // Fire the post processing event. EventHandler handler = OnPostProcessing; if (handler != null) { context.Items[CachedPathKey] = null; - return Task.Run(() => handler(this, new PostProcessingEventArgs { CachedImagePath = cachedPath })); + await Task.Run(() => handler(this, new PostProcessingEventArgs { CachedImagePath = cachedPath })); } } - - return Task.FromResult(null); } /// @@ -258,17 +258,11 @@ namespace ImageProcessor.Web.HttpModules object responseTypeObject = context.Items[CachedResponseTypeKey]; object dependencyFileObject = context.Items[CachedResponseFileDependency]; - if (responseTypeObject != null && dependencyFileObject != null) - { - string responseType = (string)responseTypeObject; - List dependencyFiles = (List)dependencyFileObject; - - // Set the headers - this.SetHeaders(context, responseType, dependencyFiles); + string responseType = responseTypeObject as string; + List dependencyFiles = dependencyFileObject as List; - context.Items[CachedResponseTypeKey] = null; - context.Items[CachedResponseFileDependency] = null; - } + // Set the headers + this.SetHeaders(context, responseType, dependencyFiles); } #region Private @@ -379,100 +373,83 @@ namespace ImageProcessor.Web.HttpModules } // Create a new cache to help process and cache the request. - DiskCache cache = new DiskCache(requestPath, fullPath, queryString); - string cachedPath = cache.CachedPath; - - // Since we are now rewriting the path we need to check again that the current user has access - // to the rewritten path. - // Get the user for the current request - // If the user is anonymous or authentication doesn't work for this suffix avoid a NullReferenceException - // in the UrlAuthorizationModule by creating a generic identity. - string virtualCachedPath = cache.VirtualCachedPath; + this.imageCache = new DiskCache2(requestPath, fullPath, queryString); - IPrincipal user = context.User ?? new GenericPrincipal(new GenericIdentity(string.Empty, string.Empty), new string[0]); + // Is the file new or updated? + bool isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync(); + string cachedPath = this.imageCache.CachedPath; - // Do we have permission to call UrlAuthorizationModule.CheckUrlAccessForPrincipal? - PermissionSet permission = new PermissionSet(PermissionState.None); - permission.AddPermission(new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted)); - bool hasPermission = permission.IsSubsetOf(AppDomain.CurrentDomain.PermissionSet); - - bool isAllowed = true; - - // Run the rewritten path past the authorization system again. - // We can then use the result as the default "AllowAccess" value - if (hasPermission && !context.SkipAuthorization) - { - isAllowed = UrlAuthorizationModule.CheckUrlAccessForPrincipal(virtualCachedPath, user, "GET"); - } - - if (isAllowed) + // Only process if the file has been updated. + if (isNewOrUpdated) { - // Is the file new or updated? - bool isNewOrUpdated = cache.IsNewOrUpdatedFile(cachedPath); - - // Only process if the file has been updated. - if (isNewOrUpdated) + // Process the image. + using (ImageFactory imageFactory = new ImageFactory(preserveExifMetaData != null && preserveExifMetaData.Value)) { - // Process the image. - using (ImageFactory imageFactory = new ImageFactory(preserveExifMetaData != null && preserveExifMetaData.Value)) + using (await this.locker.LockAsync(cachedPath)) { - using (await this.locker.LockAsync(cachedPath)) - { - byte[] imageBuffer = await currentService.GetImage(resourcePath); + byte[] imageBuffer = await currentService.GetImage(resourcePath); - using (MemoryStream memoryStream = new MemoryStream(imageBuffer)) - { - // Reset the position of the stream to ensure we're reading the correct part. - memoryStream.Position = 0; + using (MemoryStream memoryStream = new MemoryStream(imageBuffer)) + { + // Reset the position of the stream to ensure we're reading the correct part. + memoryStream.Position = 0; - // Process the Image - imageFactory.Load(memoryStream).AutoProcess(queryString).Save(cachedPath); + // Process the Image + imageFactory.Load(memoryStream).AutoProcess(queryString).Save(memoryStream); + memoryStream.Position = 0; - // Add to the cache. - cache.AddImageToCache(cachedPath); + // Add to the cache. + await this.imageCache.AddImageToCacheAsync(memoryStream); - // Store the cached path, response type, and cache dependency in the context for later retrieval. - context.Items[CachedPathKey] = cachedPath; - context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType; + // Store the cached path, response type, and cache dependency in the context for later retrieval. + context.Items[CachedPathKey] = cachedPath; + context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType; + if (isFileLocal) + { + // Some services might only provide filename so we can't monitor for the browser. + context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath + ? new List { cachedPath } + : new List { requestPath, cachedPath }; + } + else + { context.Items[CachedResponseFileDependency] = new List { cachedPath }; } } } } + } - // Image is from the cache so the mime-type will need to be set. - if (context.Items[CachedResponseTypeKey] == null) - { - string mimetype = ImageHelpers.GetMimeType(cachedPath); - - if (!string.IsNullOrEmpty(mimetype)) - { - context.Items[CachedResponseTypeKey] = mimetype; - } - } + // Image is from the cache so the mime-type will need to be set. + // TODO: Is this bit needed? Is the static file handler doing stuff for the filecache + // but not others. + if (context.Items[CachedResponseTypeKey] == null) + { + string mimetype = ImageHelpers.GetMimeType(this.imageCache.CachedPath); - if (context.Items[CachedResponseFileDependency] == null) + if (!string.IsNullOrEmpty(mimetype)) { - if (isFileLocal) - { - // Some services might only provide filename so we can't monitor for the browser. - context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath - ? new List { cachedPath } - : new List { requestPath, cachedPath }; - } - else - { - context.Items[CachedResponseFileDependency] = new List { cachedPath }; - } + context.Items[CachedResponseTypeKey] = mimetype; } - - // The cached file is valid so just rewrite the path. - context.RewritePath(virtualCachedPath, false); } - else + + if (context.Items[CachedResponseFileDependency] == null) { - throw new HttpException(403, "Access denied"); + if (isFileLocal) + { + // Some services might only provide filename so we can't monitor for the browser. + context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath + ? new List { this.imageCache.CachedPath } + : new List { requestPath, this.imageCache.CachedPath }; + } + else + { + context.Items[CachedResponseFileDependency] = new List { this.imageCache.CachedPath }; + } } + + // The cached file is valid so just rewrite the path. + this.imageCache.RewritePath(context); } } @@ -494,28 +471,34 @@ namespace ImageProcessor.Web.HttpModules { HttpResponse response = context.Response; - response.ContentType = responseType; - if (response.Headers["Image-Served-By"] == null) { response.AddHeader("Image-Served-By", "ImageProcessor.Web/" + AssemblyVersion); } - HttpCachePolicy cache = response.Cache; - cache.SetCacheability(HttpCacheability.Public); - cache.VaryByHeaders["Accept-Encoding"] = true; + if (this.imageCache != null) + { + HttpCachePolicy cache = response.Cache; + cache.SetCacheability(HttpCacheability.Public); + cache.VaryByHeaders["Accept-Encoding"] = true; - context.Response.AddFileDependencies(dependencyPaths.ToArray()); - cache.SetLastModifiedFromFileDependencies(); + if (!string.IsNullOrWhiteSpace(responseType)) + { + response.ContentType = responseType; + } - int maxDays = DiskCache.MaxFileCachedDuration; + if (dependencyPaths != null) + { + context.Response.AddFileDependencies(dependencyPaths.ToArray()); + cache.SetLastModifiedFromFileDependencies(); + } - cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(maxDays)); - cache.SetMaxAge(new TimeSpan(maxDays, 0, 0, 0)); - cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + int maxDays = this.imageCache.MaxAge; - context.Items[CachedResponseTypeKey] = null; - context.Items[CachedResponseFileDependency] = null; + cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(maxDays)); + cache.SetMaxAge(new TimeSpan(maxDays, 0, 0, 0)); + cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + } } /// diff --git a/src/ImageProcessor.Web/ImageProcessor.Web.csproj b/src/ImageProcessor.Web/ImageProcessor.Web.csproj index 9ad80addb..508bc4d43 100644 --- a/src/ImageProcessor.Web/ImageProcessor.Web.csproj +++ b/src/ImageProcessor.Web/ImageProcessor.Web.csproj @@ -46,6 +46,9 @@ + + + @@ -56,7 +59,6 @@ - diff --git a/src/packages/repositories.config b/src/packages/repositories.config index bfef3f66b..56a02b395 100644 --- a/src/packages/repositories.config +++ b/src/packages/repositories.config @@ -1,6 +1,7 @@  + \ No newline at end of file