From e9ca2c1479ae70874d595a1ae81734b56237c072 Mon Sep 17 00:00:00 2001 From: James South Date: Mon, 16 Feb 2015 12:07:36 +0000 Subject: [PATCH 1/8] Fix issue #125 Former-commit-id: 3d88825a483ed7a214b6614f0eae52778b504e92 Former-commit-id: c538ee57e5a4d9512931cc9f4b79fc24fc4532dc --- src/ImageProcessor.Web/Helpers/ImageHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageProcessor.Web/Helpers/ImageHelpers.cs b/src/ImageProcessor.Web/Helpers/ImageHelpers.cs index 6b2ea9eb5..22d73d537 100644 --- a/src/ImageProcessor.Web/Helpers/ImageHelpers.cs +++ b/src/ImageProcessor.Web/Helpers/ImageHelpers.cs @@ -67,7 +67,7 @@ namespace ImageProcessor.Web.Helpers // First check to see if the format processor is being used and test against that. IWebGraphicsProcessor format = ImageProcessorConfiguration.Instance.GraphicsProcessors - .First(p => typeof(Format) == p.GetType()); + .FirstOrDefault(p => typeof(Format) == p.GetType()); if (format != null) { From f897c50c5ea0b964a63fe191425204ec486b37ee Mon Sep 17 00:00:00 2001 From: James South Date: Mon, 16 Feb 2015 18:28:56 +0000 Subject: [PATCH 2/8] Begin decoupling cache. Former-commit-id: 89e8a1bb0ddefe6c9799ab8d58ec767b15fb95b0 Former-commit-id: a64204862f758539b40845986564ec94bb17c1b8 --- .../AzureBlobCache.cs | 98 ++++++++++ .../ImageProcessor.Web.AzureBlobCache.csproj | 95 +++++++++ .../Properties/AssemblyInfo.cs | 36 ++++ .../packages.config | 10 + src/ImageProcessor.Web/Caching/DiskCache2.cs | 180 +++++++++++++++++ src/ImageProcessor.Web/Caching/IImageCache.cs | 26 +++ .../Caching/ImageCacheBase.cs | 125 ++++++++++++ .../Extensions/DirectoryInfoExtensions.cs | 32 ++- .../HttpModules/ImageProcessingModule.cs | 185 ++++++++---------- .../ImageProcessor.Web.csproj | 4 +- src/packages/repositories.config | 1 + 11 files changed, 689 insertions(+), 103 deletions(-) create mode 100644 src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs create mode 100644 src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj create mode 100644 src/ImageProcessor.Web.AzureBlobCache/Properties/AssemblyInfo.cs create mode 100644 src/ImageProcessor.Web.AzureBlobCache/packages.config create mode 100644 src/ImageProcessor.Web/Caching/DiskCache2.cs create mode 100644 src/ImageProcessor.Web/Caching/IImageCache.cs create mode 100644 src/ImageProcessor.Web/Caching/ImageCacheBase.cs 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 From 024e95a2054b5bf7e2e97aec90a39bfd8609c010 Mon Sep 17 00:00:00 2001 From: James South Date: Tue, 17 Feb 2015 00:02:23 +0000 Subject: [PATCH 3/8] More tooling in Azure cache. TODO: Config Former-commit-id: d4bb114a2a45ee9e1cb5286d700668d9ad243cbd Former-commit-id: 12c00c04646fefc73f241ee93a7fb358ad32d2e7 --- .../AzureBlobCache.cs | 189 ++++++++++++++---- .../ImageProcessor.Web.AzureBlobCache.csproj | 4 + src/ImageProcessor.Web/Caching/DiskCache2.cs | 58 +++--- src/ImageProcessor.Web/Caching/IImageCache.cs | 6 + .../Caching/ImageCacheBase.cs | 64 +++--- src/ImageProcessor.sln | 17 ++ 6 files changed, 238 insertions(+), 100 deletions(-) diff --git a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs index 4706fd857..d6beca980 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs +++ b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs @@ -1,11 +1,17 @@ namespace ImageProcessor.Web.AzureBlobCache { using System; + using System.Collections.Generic; using System.Configuration; + using System.Globalization; + using System.IO; + using System.Linq; using System.Threading.Tasks; using System.Web; using ImageProcessor.Web.Caching; + using ImageProcessor.Web.Extensions; + using ImageProcessor.Web.Helpers; using Microsoft.WindowsAzure; using Microsoft.WindowsAzure.Storage; @@ -13,39 +19,72 @@ public class AzureBlobCache : ImageCacheBase { - private CloudStorageAccount cloudStorageAccount; + /// + /// The max age. + /// + private readonly int maxAge; + + private CloudStorageAccount cloudCachedStorageAccount; + + private CloudStorageAccount cloudSourceStorageAccount; + + private CloudBlobClient cloudCachedBlobClient; - private CloudBlobClient cloudBlobClient; + private CloudBlobClient cloudSourceBlobClient; - private CloudBlobContainer cloudBlobContainer; + private CloudBlobContainer cloudCachedBlobContainer; + + private CloudBlobContainer cloudSourceBlobContainer; + + private string cachedContainerRoot; + + /// + /// The physical cached path. + /// + private string physicalCachedPath; public AzureBlobCache(string requestPath, string fullPath, string querystring) : base(requestPath, fullPath, querystring) { - // TODO: These should all be in the configuration. + // TODO: Get from configuration. + this.Settings = new Dictionary(); - // Retrieve storage account from connection string. - this.cloudStorageAccount = CloudStorageAccount.Parse( - CloudConfigurationManager.GetSetting("StorageConnectionString")); + this.maxAge = Convert.ToInt32(this.Settings["MaxAge"]); - // Create the blob client. - this.cloudBlobClient = this.cloudStorageAccount.CreateCloudBlobClient(); + // Retrieve storage accounts from connection string. + this.cloudCachedStorageAccount = CloudStorageAccount.Parse(this.Settings["CachedStorageAccount"]); + this.cloudSourceStorageAccount = CloudStorageAccount.Parse(this.Settings["SourceStorageAccount"]); - // Retrieve reference to a previously created container. - this.cloudBlobContainer = this.cloudBlobClient.GetContainerReference("mycontainer"); + // Create the blob clients. + this.cloudCachedBlobClient = this.cloudCachedStorageAccount.CreateCloudBlobClient(); + this.cloudSourceBlobClient = this.cloudSourceStorageAccount.CreateCloudBlobClient(); + + // Retrieve references to a previously created containers. + this.cloudCachedBlobContainer = this.cloudCachedBlobClient.GetContainerReference(this.Settings["CachedBlobContainer"]); + this.cloudSourceBlobContainer = this.cloudSourceBlobClient.GetContainerReference(this.Settings["SourceBlobContainer"]); + + this.cachedContainerRoot = this.Settings["CachedContainerRoot"]; } public override int MaxAge { - get { throw new System.NotImplementedException(); } + get + { + return this.maxAge; + } } public override async Task IsNewOrUpdatedAsync() { string cachedFileName = await this.CreateCachedFileName(); - // TODO: Generate cache path. - CloudBlockBlob blockBlob = new CloudBlockBlob(new Uri("")); + // 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)); + this.CachedPath = Path.Combine(this.cachedContainerRoot, pathFromKey, cachedFileName).Replace(@"\", "/"); + + ICloudBlob blockBlob = await this.cloudCachedBlobContainer + .GetBlobReferenceFromServerAsync(this.RequestPath); bool isUpdated = false; if (!await blockBlob.ExistsAsync()) @@ -53,46 +92,126 @@ // Nothing in the cache so we should return true. isUpdated = true; } - else if (blockBlob.Properties.LastModified.HasValue) + else { - // Check to see if the cached image is set to expire. - if (this.IsExpired(blockBlob.Properties.LastModified.Value.UtcDateTime)) + // Pull the latest info. + await blockBlob.FetchAttributesAsync(); + if (blockBlob.Properties.LastModified.HasValue) { - isUpdated = true; + // 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) + public override async Task AddImageToCacheAsync(Stream stream) { - throw new System.NotImplementedException(); + CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(this.CachedPath); + await blockBlob.UploadFromStreamAsync(stream); } public override async Task TrimCacheAsync() { - throw new System.NotImplementedException(); + Uri uri = new Uri(this.CachedPath); + string path = uri.GetLeftPart(UriPartial.Path); + string directory = path.Substring(0, path.LastIndexOf('/')); + string parent = directory.Substring(0, path.LastIndexOf('/')); + + BlobContinuationToken continuationToken = null; + CloudBlobDirectory directoryBlob = this.cloudCachedBlobContainer.GetDirectoryReference(parent); + List results = new List(); + + // Loop through the all the files in a non blocking fashion. + do + { + BlobResultSegment response = await directoryBlob.ListBlobsSegmentedAsync(continuationToken); + continuationToken = response.ContinuationToken; + results.AddRange(response.Results); + } + while (continuationToken != null); + + // Now leap through and delete. + foreach (CloudBlockBlob blob in results + .Where((blobItem, type) => blobItem is CloudBlockBlob) + .Cast() + .OrderBy(b => b.Properties.LastModified != null ? b.Properties.LastModified.Value.UtcDateTime : new DateTime())) + { + if (blob.Properties.LastModified.HasValue && !this.IsExpired(blob.Properties.LastModified.Value.UtcDateTime)) + { + await blob.DeleteAsync(); + } + } } - public override void RewritePath(HttpContext context) + public override async Task CreateCachedFileName() { - throw new System.NotImplementedException(); + string streamHash = string.Empty; + + try + { + if (new Uri(this.RequestPath).IsFile) + { + ICloudBlob blockBlob = await this.cloudSourceBlobContainer + .GetBlobReferenceFromServerAsync(this.RequestPath); + + if (await blockBlob.ExistsAsync()) + { + // Pull the latest info. + await blockBlob.FetchAttributesAsync(); + + if (blockBlob.Properties.LastModified.HasValue) + { + string creation = blockBlob.Properties.LastModified.Value.UtcDateTime.ToString(CultureInfo.InvariantCulture); + string length = blockBlob.Properties.Length.ToString(CultureInfo.InvariantCulture); + streamHash = string.Format("{0}{1}", creation, length); + } + } + else + { + // 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"); + + return cachedFileName; } - /// - /// 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) + public override void RewritePath(HttpContext context) { - return creationDate.AddDays(this.MaxAge) < DateTime.UtcNow.AddDays(-this.MaxAge); + // The cached file is valid so just rewrite the path. + context.RewritePath(this.CachedPath, false); } } } diff --git a/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj b/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj index 4d30e737d..f2b15b3ab 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj +++ b/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj @@ -47,11 +47,15 @@ ..\packages\Microsoft.WindowsAzure.ConfigurationManager.1.8.0.0\lib\net35-full\Microsoft.WindowsAzure.Configuration.dll + + ..\packages\WindowsAzure.Storage.4.3.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + ..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll True + False diff --git a/src/ImageProcessor.Web/Caching/DiskCache2.cs b/src/ImageProcessor.Web/Caching/DiskCache2.cs index 2ea9e34a6..f4e04e7c3 100644 --- a/src/ImageProcessor.Web/Caching/DiskCache2.cs +++ b/src/ImageProcessor.Web/Caching/DiskCache2.cs @@ -26,29 +26,33 @@ private const int MaxFilesCount = 100; /// - /// The virtual cache path. + /// The max age. /// - private static readonly string VirtualCachePath = ImageProcessorConfiguration.Instance.VirtualCachePath; + private readonly int maxAge; /// - /// The absolute path to virtual cache path on the server. - /// TODO: Change this so configuration is determined per IImageCache instance. + /// The virtual cache path. /// - private static readonly string AbsoluteCachePath = HostingEnvironment.MapPath(VirtualCachePath); + private readonly string virtualCachePath; /// - /// The physical cached path. + /// The absolute path to virtual cache path on the server. /// - private string physicalCachedPath; + private readonly string absoluteCachePath; /// - /// The virtual cached path. + /// The virtual cached path to the cached file. /// - private string virtualCachedPath; + private string virtualCachedFilePath; public DiskCache2(string requestPath, string fullPath, string querystring) : base(requestPath, fullPath, querystring) { + // TODO: Get from configuration. + this.Settings = new Dictionary(); + this.maxAge = Convert.ToInt32(this.Settings["MaxAge"]); + this.virtualCachePath = this.Settings["VirtualCachePath"]; + this.absoluteCachePath = HostingEnvironment.MapPath(this.virtualCachePath); } /// @@ -59,7 +63,7 @@ { get { - return ImageProcessorConfiguration.Instance.MaxCacheDays; + return this.maxAge; } } @@ -68,15 +72,14 @@ string cachedFileName = await this.CreateCachedFileName(); // Collision rate of about 1 in 10000 for the folder structure. - // That gives us massive scope for files. + // That gives us massive scope to store millions of 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; + 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.physicalCachedPath); + CachedImage cachedImage = CacheIndexer.GetValue(this.CachedPath); if (cachedImage == null) { @@ -88,7 +91,7 @@ // Check to see if the cached image is set to expire. if (this.IsExpired(cachedImage.CreationTimeUtc)) { - CacheIndexer.Remove(this.physicalCachedPath); + CacheIndexer.Remove(this.CachedPath); isUpdated = true; } } @@ -99,13 +102,13 @@ public override async Task AddImageToCacheAsync(Stream stream) { // ReSharper disable once AssignNullToNotNullAttribute - DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.physicalCachedPath)); + DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.CachedPath)); if (!directoryInfo.Exists) { directoryInfo.Create(); } - using (FileStream fileStream = File.Create(this.physicalCachedPath)) + using (FileStream fileStream = File.Create(this.CachedPath)) { await stream.CopyToAsync(fileStream); } @@ -113,7 +116,7 @@ public override async Task TrimCacheAsync() { - string directory = Path.GetDirectoryName(this.physicalCachedPath); + string directory = Path.GetDirectoryName(this.CachedPath); if (directory != null) { @@ -159,22 +162,7 @@ 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); + context.RewritePath(this.virtualCachedFilePath, false); } } } diff --git a/src/ImageProcessor.Web/Caching/IImageCache.cs b/src/ImageProcessor.Web/Caching/IImageCache.cs index 6153c1f8a..d6404ff51 100644 --- a/src/ImageProcessor.Web/Caching/IImageCache.cs +++ b/src/ImageProcessor.Web/Caching/IImageCache.cs @@ -1,12 +1,18 @@  namespace ImageProcessor.Web.Caching { + using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using System.Web; public interface IImageCache { + /// + /// Gets or sets any additional settings required by the cache. + /// + Dictionary Settings { get; } + string CachedPath { get; } int MaxAge { get; } diff --git a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs index c33a03e60..cfb7ea5ce 100644 --- a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs +++ b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs @@ -1,6 +1,7 @@ namespace ImageProcessor.Web.Caching { using System; + using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; @@ -12,25 +13,25 @@ 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; + protected readonly string RequestPath; /// /// The full path for the image. /// - private readonly string fullPath; + protected readonly string FullPath; /// /// The querystring containing processing instructions. /// - private readonly string querystring; + protected readonly string Querystring; + + /// + /// The assembly version. + /// + private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); /// /// Initializes a new instance of the class. @@ -46,11 +47,16 @@ /// protected ImageCacheBase(string requestPath, string fullPath, string querystring) { - this.requestPath = requestPath; - this.fullPath = fullPath; - this.querystring = querystring; + this.RequestPath = requestPath; + this.FullPath = fullPath; + this.Querystring = querystring; } + /// + /// Gets any additional settings required by the cache. + /// + public Dictionary Settings { get; set; } + public string CachedPath { get; protected set; } public abstract int MaxAge { get; } @@ -61,17 +67,17 @@ public abstract Task TrimCacheAsync(); - public Task CreateCachedFileName() + public virtual Task CreateCachedFileName() { string streamHash = string.Empty; try { - if (new Uri(this.requestPath).IsFile) + 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); + FileInfo imageFileInfo = new FileInfo(this.RequestPath); if (imageFileInfo.Exists) { // Pull the latest info. @@ -92,34 +98,32 @@ // 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 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) + /// + /// 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. + /// + protected virtual bool IsExpired(DateTime creationDate) { - 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; + return creationDate.AddDays(this.MaxAge) < DateTime.UtcNow.AddDays(-this.MaxAge); } } } diff --git a/src/ImageProcessor.sln b/src/ImageProcessor.sln index 8caaeabc7..a5b0609f3 100644 --- a/src/ImageProcessor.sln +++ b/src/ImageProcessor.sln @@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Playground", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Web.PostProcessor", "ImageProcessor.Web.PostProcessor\ImageProcessor.Web.PostProcessor.csproj", "{55D08737-7D7E-4995-8892-BD9F944329E6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageProcessor.Web.AzureBlobCache", "ImageProcessor.Web.AzureBlobCache\ImageProcessor.Web.AzureBlobCache.csproj", "{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution All|Any CPU = All|Any CPU @@ -198,6 +200,21 @@ Global {55D08737-7D7E-4995-8892-BD9F944329E6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {55D08737-7D7E-4995-8892-BD9F944329E6}.Release|Mixed Platforms.Build.0 = Release|Any CPU {55D08737-7D7E-4995-8892-BD9F944329E6}.Release|x86.ActiveCfg = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Any CPU.ActiveCfg = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Any CPU.Build.0 = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Mixed Platforms.ActiveCfg = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|Mixed Platforms.Build.0 = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.All|x86.ActiveCfg = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Any CPU.Build.0 = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3C805E4C-D679-43F8-8C43-8909CDB4D4D7}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From e3c48bca4fc6607b090ec66842a1f2edc50de6b5 Mon Sep 17 00:00:00 2001 From: James South Date: Tue, 17 Feb 2015 13:05:36 +0000 Subject: [PATCH 4/8] Tie up cache to configuration. Former-commit-id: 1ba1d6f6fdba545f9659d634cc9bb1e6e0833758 Former-commit-id: 192eccd79658cb84e31c22e23c0f4c9690dbe314 --- .../AzureBlobCache.cs | 66 ++-- .../Caching/CacheIndexer.cs | 30 +- src/ImageProcessor.Web/Caching/CachedImage.cs | 2 +- src/ImageProcessor.Web/Caching/DiskCache.cs | 297 ++++++------------ src/ImageProcessor.Web/Caching/DiskCache2.cs | 168 ---------- src/ImageProcessor.Web/Caching/IImageCache.cs | 4 +- .../Caching/ImageCacheBase.cs | 13 +- .../Configuration/ImageCacheSection.cs | 155 +++++++-- .../Configuration/ImageProcessingSection.cs | 156 +-------- .../ImageProcessorConfiguration.cs | 72 +++-- .../Configuration/ImageSecuritySection.cs | 171 +--------- .../Configuration/Resources/cache.config | 11 +- .../Configuration/Shared/SettingElement.cs | 56 ++++ .../Shared/SettingElementCollection.cs | 117 +++++++ .../TypeInitializationExtensions.cs | 180 +++++++++++ .../Helpers/TypePropertyHelpers.cs | 19 ++ .../HttpModules/ImageProcessingModule.cs | 38 ++- .../ImageProcessor.Web.csproj | 6 +- .../ImageProcessor.Web.csproj.DotSettings | 2 + src/TestWebsites/MVC/Test_Website_MVC.csproj | 4 + .../MVC/config/imageprocessor/cache.config | 12 +- 21 files changed, 775 insertions(+), 804 deletions(-) delete mode 100644 src/ImageProcessor.Web/Caching/DiskCache2.cs create mode 100644 src/ImageProcessor.Web/Configuration/Shared/SettingElement.cs create mode 100644 src/ImageProcessor.Web/Configuration/Shared/SettingElementCollection.cs create mode 100644 src/ImageProcessor.Web/Extensions/TypeInitializationExtensions.cs create mode 100644 src/ImageProcessor.Web/Helpers/TypePropertyHelpers.cs create mode 100644 src/ImageProcessor.Web/ImageProcessor.Web.csproj.DotSettings diff --git a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs index d6beca980..3820f421a 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs +++ b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs @@ -22,7 +22,7 @@ /// /// The max age. /// - private readonly int maxAge; + private readonly int maxDays; private CloudStorageAccount cloudCachedStorageAccount; @@ -46,10 +46,7 @@ public AzureBlobCache(string requestPath, string fullPath, string querystring) : base(requestPath, fullPath, querystring) { - // TODO: Get from configuration. - this.Settings = new Dictionary(); - - this.maxAge = Convert.ToInt32(this.Settings["MaxAge"]); + this.maxDays = Convert.ToInt32(this.Settings["MaxAge"]); // Retrieve storage accounts from connection string. this.cloudCachedStorageAccount = CloudStorageAccount.Parse(this.Settings["CachedStorageAccount"]); @@ -66,11 +63,11 @@ this.cachedContainerRoot = this.Settings["CachedContainerRoot"]; } - public override int MaxAge + public override int MaxDays { get { - return this.maxAge; + return this.maxDays; } } @@ -83,24 +80,44 @@ string pathFromKey = string.Join("\\", cachedFileName.ToCharArray().Take(6)); this.CachedPath = Path.Combine(this.cachedContainerRoot, pathFromKey, cachedFileName).Replace(@"\", "/"); - ICloudBlob blockBlob = await this.cloudCachedBlobContainer - .GetBlobReferenceFromServerAsync(this.RequestPath); - bool isUpdated = false; - if (!await blockBlob.ExistsAsync()) - { - // Nothing in the cache so we should return true. - isUpdated = true; - } - else + CachedImage cachedImage = CacheIndexer.GetValue(this.CachedPath); + + if (cachedImage == null) { - // Pull the latest info. - await blockBlob.FetchAttributesAsync(); - if (blockBlob.Properties.LastModified.HasValue) + ICloudBlob blockBlob = + await this.cloudCachedBlobContainer.GetBlobReferenceFromServerAsync(this.RequestPath); + + if (await blockBlob.ExistsAsync()) + { + // Pull the latest info. + await blockBlob.FetchAttributesAsync(); + + if (blockBlob.Properties.LastModified.HasValue) + { + cachedImage = new CachedImage + { + Key = Path.GetFileNameWithoutExtension(this.CachedPath), + Path = this.CachedPath, + CreationTimeUtc = + blockBlob.Properties.LastModified.Value.UtcDateTime + }; + + 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(blockBlob.Properties.LastModified.Value.UtcDateTime)) + if (this.IsExpired(cachedImage.CreationTimeUtc)) { + CacheIndexer.Remove(this.CachedPath); isUpdated = true; } } @@ -141,8 +158,15 @@ .Cast() .OrderBy(b => b.Properties.LastModified != null ? b.Properties.LastModified.Value.UtcDateTime : new DateTime())) { - if (blob.Properties.LastModified.HasValue && !this.IsExpired(blob.Properties.LastModified.Value.UtcDateTime)) + if (blob.Properties.LastModified.HasValue + && !this.IsExpired(blob.Properties.LastModified.Value.UtcDateTime)) + { + break; + } + else { + // Remove from the cache and delete each CachedImage. + CacheIndexer.Remove(blob.Name); await blob.DeleteAsync(); } } diff --git a/src/ImageProcessor.Web/Caching/CacheIndexer.cs b/src/ImageProcessor.Web/Caching/CacheIndexer.cs index e69e65c59..8a7786eb3 100644 --- a/src/ImageProcessor.Web/Caching/CacheIndexer.cs +++ b/src/ImageProcessor.Web/Caching/CacheIndexer.cs @@ -15,9 +15,9 @@ namespace ImageProcessor.Web.Caching using System.Runtime.Caching; /// - /// Represents an in memory collection of keys and values whose operations are concurrent. + /// Represents an in memory collection of cached images whose operations are concurrent. /// - internal static class CacheIndexer + public static class CacheIndexer { #region Public /// @@ -34,30 +34,6 @@ namespace ImageProcessor.Web.Caching { string key = Path.GetFileNameWithoutExtension(cachedPath); CachedImage cachedImage = (CachedImage)MemCache.GetItem(key); - - if (cachedImage == null) - { - // FileInfo is thread safe. - FileInfo fileInfo = new FileInfo(cachedPath); - - if (!fileInfo.Exists) - { - return null; - } - - // Pull the latest info. - fileInfo.Refresh(); - - cachedImage = new CachedImage - { - Key = Path.GetFileNameWithoutExtension(cachedPath), - Path = cachedPath, - CreationTimeUtc = fileInfo.CreationTimeUtc - }; - - Add(cachedImage); - } - return cachedImage; } @@ -92,7 +68,7 @@ namespace ImageProcessor.Web.Caching CacheItemPolicy policy = new CacheItemPolicy(); policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List { cachedImage.Path })); - MemCache.AddItem(cachedImage.Key, cachedImage, policy); + MemCache.AddItem(Path.GetFileNameWithoutExtension(cachedImage.Key), cachedImage, policy); return cachedImage; } #endregion diff --git a/src/ImageProcessor.Web/Caching/CachedImage.cs b/src/ImageProcessor.Web/Caching/CachedImage.cs index 511c2b318..25519fcc4 100644 --- a/src/ImageProcessor.Web/Caching/CachedImage.cs +++ b/src/ImageProcessor.Web/Caching/CachedImage.cs @@ -15,7 +15,7 @@ namespace ImageProcessor.Web.Caching /// /// Describes a cached image /// - internal sealed class CachedImage + public sealed class CachedImage { /// /// Gets or sets the key identifying the cached image. diff --git a/src/ImageProcessor.Web/Caching/DiskCache.cs b/src/ImageProcessor.Web/Caching/DiskCache.cs index fd0778e76..e0e2ec555 100644 --- a/src/ImageProcessor.Web/Caching/DiskCache.cs +++ b/src/ImageProcessor.Web/Caching/DiskCache.cs @@ -1,39 +1,19 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. -// -// -// The disk cache. -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace ImageProcessor.Web.Caching +namespace ImageProcessor.Web.Caching { - #region Using using System; using System.Collections.Generic; - using System.Globalization; + using System.Configuration; 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; - using ImageProcessor.Web.Helpers; - #endregion - /// - /// The disk cache. - /// - internal sealed class DiskCache + public class DiskCache : ImageCacheBase { - #region Fields - /// - /// The maximum number of days to cache files on the system for. - /// - internal static readonly int MaxFileCachedDuration = ImageProcessorConfiguration.Instance.MaxCacheDays; - /// /// The maximum number of files allowed in the directory. /// @@ -47,42 +27,25 @@ namespace ImageProcessor.Web.Caching 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. - /// - private static readonly string AbsoluteCachePath = HostingEnvironment.MapPath(ImageProcessorConfiguration.Instance.VirtualCachePath); - - /// - /// The request path for the image. - /// - private readonly string requestPath; - - /// - /// The full path for the image. + /// The max age. /// - private readonly string fullPath; + private readonly int maxDays; /// - /// The querystring containing processing instructions. + /// The virtual cache path. /// - private readonly string querystring; + private readonly string virtualCachePath; /// - /// The physical cached path. + /// The absolute path to virtual cache path on the server. /// - private string physicalCachedPath; + private readonly string absoluteCachePath; /// - /// The virtual cached path. + /// The virtual cached path to the cached file. /// - private string virtualCachedPath; - #endregion + private string virtualCachedFilePath; - #region Constructors /// /// Initializes a new instance of the class. /// @@ -96,49 +59,103 @@ namespace ImageProcessor.Web.Caching /// The querystring containing instructions. /// public DiskCache(string requestPath, string fullPath, string querystring) + : base(requestPath, fullPath, querystring) { - this.requestPath = requestPath; - this.fullPath = fullPath; - this.querystring = querystring; + this.maxDays = Convert.ToInt32(this.Settings["MaxAge"]); + string virtualPath = this.Settings["VirtualCachePath"]; + + if (!virtualPath.IsValidVirtualPathName()) + { + throw new ConfigurationErrorsException("DiskCache 'VirtualCachePath' is not a valid virtual path."); + } - // Get the physical and virtual paths. - this.GetCachePaths(); + this.virtualCachePath = virtualPath; + + this.absoluteCachePath = HostingEnvironment.MapPath(this.virtualCachePath); } - #endregion /// - /// Gets the cached path. + /// The maximum number of days to cache files on the system for. + /// TODO: Shift the getter source to proper config. /// - public string CachedPath + public override int MaxDays { get { - return this.physicalCachedPath; + return this.maxDays; } } - /// - /// Gets the cached path. - /// - public string VirtualCachedPath + public override async Task IsNewOrUpdatedAsync() { - get + 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 { - return this.virtualCachedPath; + // Check to see if the cached image is set to expire. + if (this.IsExpired(cachedImage.CreationTimeUtc)) + { + CacheIndexer.Remove(this.CachedPath); + isUpdated = true; + } } + + return isUpdated; } - #region Methods - #region Public - /// - /// Trims a cached folder ensuring that it does not exceed the maximum file count. - /// - /// - /// The path to the folder. - /// - public static void TrimCachedFolders(string path) + public override async Task AddImageToCacheAsync(Stream stream) { - string directory = Path.GetDirectoryName(path); + // 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); + } + } + + public override async Task TrimCacheAsync() + { + string directory = Path.GetDirectoryName(this.CachedPath); if (directory != null) { @@ -148,7 +165,7 @@ namespace ImageProcessor.Web.Caching if (parentDirectoryInfo != null) { // UNC folders can throw exceptions if the file doesn't exist. - foreach (DirectoryInfo enumerateDirectory in parentDirectoryInfo.SafeEnumerateDirectories()) + foreach (DirectoryInfo enumerateDirectory in await parentDirectoryInfo.SafeEnumerateDirectoriesAsync()) { IEnumerable files = enumerateDirectory.EnumerateFiles().OrderBy(f => f.CreationTimeUtc); int count = files.Count(); @@ -160,7 +177,7 @@ namespace ImageProcessor.Web.Caching // 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) + if (!this.IsExpired(fileInfo.CreationTimeUtc) && count <= MaxFilesCount - 1) { break; } @@ -181,130 +198,10 @@ namespace ImageProcessor.Web.Caching } } - /// - /// Adds an image to the cache. - /// - /// - /// The path to the cached image. - /// - public void AddImageToCache(string cachedPath) - { - string key = Path.GetFileNameWithoutExtension(cachedPath); - CachedImage cachedImage = new CachedImage - { - Key = key, - Path = cachedPath, - CreationTimeUtc = DateTime.UtcNow - }; - - CacheIndexer.Add(cachedImage); - } - - /// - /// Returns a value indicating whether the original file is new or has been updated. - /// - /// - /// The path to the cached image. - /// - /// - /// True if The original file is new or has been updated; otherwise, false. - /// - public bool IsNewOrUpdatedFile(string cachedPath) + public override void RewritePath(HttpContext context) { - bool isUpdated = false; - CachedImage cachedImage = CacheIndexer.GetValue(cachedPath); - - 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 (IsExpired(cachedImage.CreationTimeUtc)) - { - CacheIndexer.Remove(cachedPath); - isUpdated = true; - } - } - - return isUpdated; - } - #endregion - - #region Private - /// - /// 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 static bool IsExpired(DateTime creationDate) - { - return creationDate.AddDays(MaxFileCachedDuration) < DateTime.UtcNow.AddDays(-MaxFileCachedDuration); - } - - /// - /// Gets the full transformed cached paths for the image. - /// The images are stored in paths that are based upon the SHA1 of their full request path - /// taking the individual characters of the hash to determine their location. - /// This allows us to store millions of images. - /// - private void GetCachePaths() - { - string streamHash = string.Empty; - - if (AbsoluteCachePath != null) - { - 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(); - - // Collision rate of about 1 in 10000 for the folder structure. - string pathFromKey = string.Join("\\", encryptedName.ToCharArray().Take(6)); - string virtualPathFromKey = pathFromKey.Replace(@"\", "/"); - - string cachedFileName = string.Format( - "{0}.{1}", - encryptedName, - !string.IsNullOrWhiteSpace(parsedExtension) ? parsedExtension.Replace(".", string.Empty) : "jpg"); - - this.physicalCachedPath = Path.Combine(AbsoluteCachePath, pathFromKey, cachedFileName); - this.virtualCachedPath = Path.Combine(VirtualCachePath, virtualPathFromKey, cachedFileName).Replace(@"\", "/"); - } + // The cached file is valid so just rewrite the path. + context.RewritePath(this.virtualCachedFilePath, false); } - #endregion - #endregion } } diff --git a/src/ImageProcessor.Web/Caching/DiskCache2.cs b/src/ImageProcessor.Web/Caching/DiskCache2.cs deleted file mode 100644 index f4e04e7c3..000000000 --- a/src/ImageProcessor.Web/Caching/DiskCache2.cs +++ /dev/null @@ -1,168 +0,0 @@ -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 max age. - /// - private readonly int maxAge; - - /// - /// The virtual cache path. - /// - private readonly string virtualCachePath; - - /// - /// The absolute path to virtual cache path on the server. - /// - private readonly string absoluteCachePath; - - /// - /// The virtual cached path to the cached file. - /// - private string virtualCachedFilePath; - - public DiskCache2(string requestPath, string fullPath, string querystring) - : base(requestPath, fullPath, querystring) - { - // TODO: Get from configuration. - this.Settings = new Dictionary(); - this.maxAge = Convert.ToInt32(this.Settings["MaxAge"]); - this.virtualCachePath = this.Settings["VirtualCachePath"]; - this.absoluteCachePath = HostingEnvironment.MapPath(this.virtualCachePath); - } - - /// - /// 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 this.maxAge; - } - } - - 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) - { - // 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; - } - - public override async Task AddImageToCacheAsync(Stream stream) - { - // 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); - } - } - - 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. - } - } - } - } - } - } - - public override void RewritePath(HttpContext context) - { - // The cached file is valid so just rewrite the path. - context.RewritePath(this.virtualCachedFilePath, false); - } - } -} diff --git a/src/ImageProcessor.Web/Caching/IImageCache.cs b/src/ImageProcessor.Web/Caching/IImageCache.cs index d6404ff51..3b2d1d337 100644 --- a/src/ImageProcessor.Web/Caching/IImageCache.cs +++ b/src/ImageProcessor.Web/Caching/IImageCache.cs @@ -11,11 +11,11 @@ namespace ImageProcessor.Web.Caching /// /// Gets or sets any additional settings required by the cache. /// - Dictionary Settings { get; } + Dictionary Settings { get; set; } string CachedPath { get; } - int MaxAge { get; } + int MaxDays { get; } Task IsNewOrUpdatedAsync(); diff --git a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs index cfb7ea5ce..84517be32 100644 --- a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs +++ b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using System.Web; + using ImageProcessor.Web.Configuration; using ImageProcessor.Web.Extensions; using ImageProcessor.Web.Helpers; @@ -28,11 +29,6 @@ /// protected readonly string Querystring; - /// - /// The assembly version. - /// - private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - /// /// Initializes a new instance of the class. /// @@ -50,16 +46,17 @@ this.RequestPath = requestPath; this.FullPath = fullPath; this.Querystring = querystring; + this.Settings = ImageProcessorConfiguration.Instance.ImageCacheSettings; } /// - /// Gets any additional settings required by the cache. + /// Gets or sets any additional settings required by the cache. /// public Dictionary Settings { get; set; } public string CachedPath { get; protected set; } - public abstract int MaxAge { get; } + public abstract int MaxDays { get; } public abstract Task IsNewOrUpdatedAsync(); @@ -123,7 +120,7 @@ /// protected virtual bool IsExpired(DateTime creationDate) { - return creationDate.AddDays(this.MaxAge) < DateTime.UtcNow.AddDays(-this.MaxAge); + return creationDate.AddDays(this.MaxDays) < DateTime.UtcNow.AddDays(-this.MaxDays); } } } diff --git a/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs b/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs index 1f868fc04..2aa05cc04 100644 --- a/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs +++ b/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs @@ -14,7 +14,6 @@ namespace ImageProcessor.Web.Configuration using System.IO; using System.Xml; - using ImageProcessor.Web.Extensions; using ImageProcessor.Web.Helpers; /// @@ -23,43 +22,34 @@ namespace ImageProcessor.Web.Configuration public sealed class ImageCacheSection : ConfigurationSection { /// - /// Gets or sets the virtual path of the cache folder. + /// Gets or sets the name of the current cache provider. /// /// The name of the cache folder. - [ConfigurationProperty("virtualPath", DefaultValue = "~/app_data/cache", IsRequired = true)] - [StringValidator(MinLength = 3, MaxLength = 256)] - public string VirtualPath + [ConfigurationProperty("currentCache", DefaultValue = "DiskCache", IsRequired = true)] + public string CurrentCache { get { - string virtualPath = (string)this["virtualPath"]; - - return virtualPath.IsValidVirtualPathName() ? virtualPath : "~/app_data/cache"; + return (string)this["currentCache"]; } set { - this["virtualPath"] = value; + this["currentCache"] = value; } } /// - /// Gets or sets the maximum number of days to store an image in the cache. + /// Gets the /// - /// The maximum number of days to store an image in the cache. - /// Defaults to 28 if not set. - [ConfigurationProperty("maxDays", DefaultValue = "365", IsRequired = false)] - [IntegerValidator(ExcludeRange = false, MinValue = 0)] - public int MaxDays + /// The + [ConfigurationProperty("caches", IsRequired = true)] + public CacheElementCollection ImageCaches { get { - return (int)this["maxDays"]; - } - - set - { - this["maxDays"] = value; + object o = this["caches"]; + return o as CacheElementCollection; } } @@ -83,5 +73,128 @@ namespace ImageProcessor.Web.Configuration return imageCacheSection; } + + /// + /// Represents a CacheElement configuration element within the configuration. + /// + public class CacheElement : ConfigurationElement + { + /// + /// Gets or sets the name of the cache. + /// + /// The name of the service. + [ConfigurationProperty("name", DefaultValue = "", IsRequired = true)] + public string Name + { + get { return (string)this["name"]; } + + set { this["name"] = value; } + } + + /// + /// Gets or sets the type of the cache. + /// + /// The full Type definition of the service + [ConfigurationProperty("type", DefaultValue = "", IsRequired = true)] + public string Type + { + get { return (string)this["type"]; } + + set { this["type"] = value; } + } + + /// + /// Gets the . + /// + /// + /// The . + /// + [ConfigurationProperty("settings", IsRequired = false)] + public SettingElementCollection Settings + { + get + { + return this["settings"] as SettingElementCollection; + } + } + } + + /// + /// Represents a collection of elements within the configuration. + /// + public class CacheElementCollection : ConfigurationElementCollection + { + /// + /// Gets the type of the . + /// + /// + /// The of this collection. + /// + public override ConfigurationElementCollectionType CollectionType + { + get { return ConfigurationElementCollectionType.BasicMap; } + } + + /// + /// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class. + /// + /// + /// The name of the collection; otherwise, an empty string. The default is an empty string. + /// + protected override string ElementName + { + get { return "cache"; } + } + + /// + /// Gets or sets the + /// at the specified index within the collection. + /// + /// The index at which to get the specified object. + /// + /// The + /// at the specified index within the collection. + /// + public CacheElement this[int index] + { + get + { + return (CacheElement)BaseGet(index); + } + + set + { + if (this.BaseGet(index) != null) + { + this.BaseRemoveAt(index); + } + + this.BaseAdd(index, value); + } + } + + /// + /// When overridden in a derived class, creates a new . + /// + /// + /// A new . + /// + protected override ConfigurationElement CreateNewElement() + { + return new CacheElement(); + } + + /// + /// Gets the element key for a specified configuration element when overridden in a derived class. + /// + /// + /// An that acts as the key for the specified . + /// + /// The to return the key for. + protected override object GetElementKey(ConfigurationElement element) + { + return ((CacheElement)element).Name; + } + } } } diff --git a/src/ImageProcessor.Web/Configuration/ImageProcessingSection.cs b/src/ImageProcessor.Web/Configuration/ImageProcessingSection.cs index cbed1468d..26fb47b8a 100644 --- a/src/ImageProcessor.Web/Configuration/ImageProcessingSection.cs +++ b/src/ImageProcessor.Web/Configuration/ImageProcessingSection.cs @@ -131,10 +131,10 @@ namespace ImageProcessor.Web.Configuration public class PresetElementCollection : ConfigurationElementCollection { /// - /// Gets the type of the . + /// Gets the type of the . /// /// - /// The of this collection. + /// The of this collection. /// public override ConfigurationElementCollectionType CollectionType { @@ -194,7 +194,7 @@ namespace ImageProcessor.Web.Configuration /// Gets the element key for a specified PluginElement configuration element. /// /// - /// The ConfigurationElement + /// The ConfigurationElement /// to return the key for. /// /// The element key for a specified PluginElement configuration element. @@ -255,10 +255,10 @@ namespace ImageProcessor.Web.Configuration public class PluginElementCollection : ConfigurationElementCollection { /// - /// Gets the type of the . + /// Gets the type of the . /// /// - /// The of this collection. + /// The of this collection. /// public override ConfigurationElementCollectionType CollectionType { @@ -318,7 +318,7 @@ namespace ImageProcessor.Web.Configuration /// Gets the element key for a specified PluginElement configuration element. /// /// - /// The ConfigurationElement + /// The ConfigurationElement /// to return the key for. /// /// The element key for a specified PluginElement configuration element. @@ -327,149 +327,5 @@ namespace ImageProcessor.Web.Configuration return ((PluginElement)element).Name; } } - - /// - /// Represents a SettingElement configuration element within the configuration. - /// - public class SettingElement : ConfigurationElement - { - /// - /// Gets or sets the key of the plugin setting. - /// - /// The key of the plugin setting. - [ConfigurationProperty("key", IsRequired = true, IsKey = true)] - public string Key - { - get - { - return this["key"] as string; - } - - set - { - this["key"] = value; - } - } - - /// - /// Gets or sets the value of the plugin setting. - /// - /// The value of the plugin setting. - [ConfigurationProperty("value", IsRequired = true)] - public string Value - { - get - { - return (string)this["value"]; - } - - set - { - this["value"] = value; - } - } - } - - /// - /// Represents a SettingElementCollection collection configuration element within the configuration. - /// - public class SettingElementCollection : ConfigurationElementCollection - { - /// - /// Gets the type of the . - /// - /// - /// The of this collection. - /// - public override ConfigurationElementCollectionType CollectionType - { - get { return ConfigurationElementCollectionType.BasicMap; } - } - - /// - /// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class. - /// - /// - /// The name of the collection; otherwise, an empty string. The default is an empty string. - /// - protected override string ElementName - { - get { return "setting"; } - } - - /// - /// Gets or sets the - /// at the specified index within the collection. - /// - /// The index at which to get the specified object. - /// - /// The - /// at the specified index within the collection. - /// - public SettingElement this[int index] - { - get - { - return (SettingElement)BaseGet(index); - } - - set - { - if (this.BaseGet(index) != null) - { - this.BaseRemoveAt(index); - } - - this.BaseAdd(index, value); - } - } - - /// - /// Returns the setting element with the specified key. - /// - /// the key representing the element - /// the setting element - public new SettingElement this[string key] - { - get { return (SettingElement)BaseGet(key); } - } - - /// - /// Returns a value indicating whether the settings collection contains the - /// given object. - /// - /// The key to identify the setting. - /// True if the collection contains the key; otherwise false. - public bool ContainsKey(string key) - { - object[] keys = BaseGetAllKeys(); - - return keys.Any(obj => (string)obj == key); - } - - /// - /// Gets the element key for a specified PluginElement configuration element. - /// - /// - /// The ConfigurationElement - /// to return the key for. - /// - /// The element key for a specified PluginElement configuration element. - protected override object GetElementKey(ConfigurationElement element) - { - return ((SettingElement)element).Key; - } - - /// - /// Creates a new SettingElement configuration element. - /// - /// - /// A new SettingElement configuration element. - /// - protected override ConfigurationElement CreateNewElement() - { - return new SettingElement(); - } - } } } diff --git a/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs b/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs index 4846c61ea..aaa3a3c4f 100644 --- a/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs +++ b/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs @@ -19,6 +19,7 @@ namespace ImageProcessor.Web.Configuration using ImageProcessor.Common.Extensions; using ImageProcessor.Processors; + using ImageProcessor.Web.Caching; using ImageProcessor.Web.Processors; using ImageProcessor.Web.Services; @@ -67,6 +68,7 @@ namespace ImageProcessor.Web.Configuration { this.LoadGraphicsProcessors(); this.LoadImageServices(); + this.LoadImageCache(); } #endregion @@ -93,41 +95,26 @@ namespace ImageProcessor.Web.Configuration public IList ImageServices { get; private set; } /// - /// Gets a value indicating whether to preserve exif meta data. + /// Gets the current image cache. /// - public bool PreserveExifMetaData - { - get - { - return GetImageProcessingSection().PreserveExifMetaData; - } - } + public Type ImageCache { get; private set; } - #region Caching /// - /// Gets the maximum number of days to store images in the cache. + /// Gets the image cache settings. /// - public int MaxCacheDays - { - get - { - return GetImageCacheSection().MaxDays; - } - } + public Dictionary ImageCacheSettings { get; private set; } /// - /// Gets or the virtual path of the cache folder. + /// Gets a value indicating whether to preserve exif meta data. /// - /// The virtual path of the cache folder. - public string VirtualCachePath + public bool PreserveExifMetaData { get { - return GetImageCacheSection().VirtualPath; + return GetImageProcessingSection().PreserveExifMetaData; } } #endregion - #endregion #region Methods /// @@ -271,7 +258,7 @@ namespace ImageProcessor.Web.Configuration if (pluginElement != null) { settings = pluginElement.Settings - .Cast() + .Cast() .ToDictionary(setting => setting.Key, setting => setting.Value); } else @@ -367,13 +354,13 @@ namespace ImageProcessor.Web.Configuration } /// - /// Returns the for the given plugin. + /// Returns the for the given plugin. /// /// /// The name of the plugin to get the settings for. /// /// - /// The for the given plugin. + /// The for the given plugin. /// private Dictionary GetServiceSettings(string name) { @@ -387,7 +374,7 @@ namespace ImageProcessor.Web.Configuration if (serviceElement != null) { settings = serviceElement.Settings - .Cast() + .Cast() .ToDictionary(setting => setting.Key, setting => setting.Value); } else @@ -424,6 +411,39 @@ namespace ImageProcessor.Web.Configuration return whitelist; } #endregion + + #region ImageCaches + /// + /// Gets the currently assigned . + /// + private void LoadImageCache() + { + if (this.ImageCache == null) + { + string curentCache = GetImageCacheSection().CurrentCache; + ImageCacheSection.CacheElementCollection caches = imageCacheSection.ImageCaches; + + foreach (ImageCacheSection.CacheElement cache in caches) + { + if (cache.Name == curentCache) + { + Type type = Type.GetType(cache.Type); + + if (type == null) + { + throw new TypeLoadException("Couldn't load IImageCache: " + cache.Type); + } + + this.ImageCache = type; + this.ImageCacheSettings = cache.Settings + .Cast() + .ToDictionary(setting => setting.Key, setting => setting.Value); + break; + } + } + } + } + #endregion #endregion } } \ No newline at end of file diff --git a/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs b/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs index c99fcb3ae..2795cf710 100644 --- a/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs +++ b/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs @@ -13,7 +13,6 @@ namespace ImageProcessor.Web.Configuration using System; using System.Configuration; using System.IO; - using System.Linq; using System.Xml; using ImageProcessor.Web.Helpers; @@ -23,11 +22,10 @@ namespace ImageProcessor.Web.Configuration /// public sealed class ImageSecuritySection : ConfigurationSection { - #region Properties /// - /// Gets the + /// Gets the /// - /// The + /// The [ConfigurationProperty("services", IsRequired = true)] public ServiceElementCollection ImageServices { @@ -42,9 +40,7 @@ namespace ImageProcessor.Web.Configuration /// Gets or sets a value indicating whether to auto load services. /// public bool AutoLoadServices { get; set; } - #endregion - #region Methods /// /// Retrieves the security configuration section from the current application configuration. /// @@ -66,7 +62,6 @@ namespace ImageProcessor.Web.Configuration imageSecuritySection.AutoLoadServices = true; return imageSecuritySection; } - #endregion /// /// Represents a ServiceElement configuration element within the configuration. @@ -110,10 +105,10 @@ namespace ImageProcessor.Web.Configuration } /// - /// Gets the . + /// Gets the . /// /// - /// The . + /// The . /// [ConfigurationProperty("settings", IsRequired = false)] public SettingElementCollection Settings @@ -146,10 +141,10 @@ namespace ImageProcessor.Web.Configuration public class ServiceElementCollection : ConfigurationElementCollection { /// - /// Gets the type of the . + /// Gets the type of the . /// /// - /// The of this collection. + /// The of this collection. /// public override ConfigurationElementCollectionType CollectionType { @@ -195,10 +190,10 @@ namespace ImageProcessor.Web.Configuration } /// - /// When overridden in a derived class, creates a new . + /// When overridden in a derived class, creates a new . /// /// - /// A new . + /// A new . /// protected override ConfigurationElement CreateNewElement() { @@ -209,159 +204,15 @@ namespace ImageProcessor.Web.Configuration /// Gets the element key for a specified configuration element when overridden in a derived class. /// /// - /// An that acts as the key for the specified . + /// An that acts as the key for the specified . /// - /// The to return the key for. + /// The to return the key for. protected override object GetElementKey(ConfigurationElement element) { return ((ServiceElement)element).Name; } } - /// - /// Represents a SettingElement configuration element within the configuration. - /// - public class SettingElement : ConfigurationElement - { - /// - /// Gets or sets the key of the plugin setting. - /// - /// The key of the plugin setting. - [ConfigurationProperty("key", IsRequired = true, IsKey = true)] - public string Key - { - get - { - return this["key"] as string; - } - - set - { - this["key"] = value; - } - } - - /// - /// Gets or sets the value of the plugin setting. - /// - /// The value of the plugin setting. - [ConfigurationProperty("value", IsRequired = true)] - public string Value - { - get - { - return (string)this["value"]; - } - - set - { - this["value"] = value; - } - } - } - - /// - /// Represents a SettingElementCollection collection configuration element within the configuration. - /// - public class SettingElementCollection : ConfigurationElementCollection - { - /// - /// Gets the type of the . - /// - /// - /// The of this collection. - /// - public override ConfigurationElementCollectionType CollectionType - { - get { return ConfigurationElementCollectionType.BasicMap; } - } - - /// - /// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class. - /// - /// - /// The name of the collection; otherwise, an empty string. The default is an empty string. - /// - protected override string ElementName - { - get { return "setting"; } - } - - /// - /// Gets or sets the - /// at the specified index within the collection. - /// - /// The index at which to get the specified object. - /// - /// The - /// at the specified index within the collection. - /// - public SettingElement this[int index] - { - get - { - return (SettingElement)BaseGet(index); - } - - set - { - if (this.BaseGet(index) != null) - { - this.BaseRemoveAt(index); - } - - this.BaseAdd(index, value); - } - } - - /// - /// Returns the setting element with the specified key. - /// - /// the key representing the element - /// the setting element - public new SettingElement this[string key] - { - get { return (SettingElement)BaseGet(key); } - } - - /// - /// Returns a value indicating whether the settings collection contains the - /// given object. - /// - /// The key to identify the setting. - /// True if the collection contains the key; otherwise false. - public bool ContainsKey(string key) - { - object[] keys = BaseGetAllKeys(); - - return keys.Any(obj => (string)obj == key); - } - - /// - /// Gets the element key for a specified PluginElement configuration element. - /// - /// - /// The ConfigurationElement - /// to return the key for. - /// - /// The element key for a specified PluginElement configuration element. - protected override object GetElementKey(ConfigurationElement element) - { - return ((SettingElement)element).Key; - } - - /// - /// Creates a new SettingElement configuration element. - /// - /// - /// A new SettingElement configuration element. - /// - protected override ConfigurationElement CreateNewElement() - { - return new SettingElement(); - } - } - /// /// Represents a whitelist collection configuration element within the configuration. /// @@ -404,7 +255,7 @@ namespace ImageProcessor.Web.Configuration /// /// Gets the element key for a specified whitelist configuration element. /// - /// The ConfigurationElement to return the key for. + /// The ConfigurationElement to return the key for. /// The element key for a specified whitelist configuration element. protected override object GetElementKey(ConfigurationElement element) { diff --git a/src/ImageProcessor.Web/Configuration/Resources/cache.config b/src/ImageProcessor.Web/Configuration/Resources/cache.config index c9b64a68a..f54e36d7a 100644 --- a/src/ImageProcessor.Web/Configuration/Resources/cache.config +++ b/src/ImageProcessor.Web/Configuration/Resources/cache.config @@ -1 +1,10 @@ - + + + + + + + + + + diff --git a/src/ImageProcessor.Web/Configuration/Shared/SettingElement.cs b/src/ImageProcessor.Web/Configuration/Shared/SettingElement.cs new file mode 100644 index 000000000..804d77ec0 --- /dev/null +++ b/src/ImageProcessor.Web/Configuration/Shared/SettingElement.cs @@ -0,0 +1,56 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Represents a SettingElement configuration element within the configuration. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Web.Configuration +{ + using System.Configuration; + + /// + /// Represents a SettingElement configuration element within the configuration. + /// + public class SettingElement : ConfigurationElement + { + /// + /// Gets or sets the key of the plugin setting. + /// + /// The key of the plugin setting. + [ConfigurationProperty("key", IsRequired = true, IsKey = true)] + public string Key + { + get + { + return this["key"] as string; + } + + set + { + this["key"] = value; + } + } + + /// + /// Gets or sets the value of the plugin setting. + /// + /// The value of the plugin setting. + [ConfigurationProperty("value", IsRequired = true)] + public string Value + { + get + { + return (string)this["value"]; + } + + set + { + this["value"] = value; + } + } + } +} diff --git a/src/ImageProcessor.Web/Configuration/Shared/SettingElementCollection.cs b/src/ImageProcessor.Web/Configuration/Shared/SettingElementCollection.cs new file mode 100644 index 000000000..d060a200e --- /dev/null +++ b/src/ImageProcessor.Web/Configuration/Shared/SettingElementCollection.cs @@ -0,0 +1,117 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Represents a SettingElementCollection collection configuration element within the configuration. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Web.Configuration +{ + using System.Configuration; + using System.Linq; + + /// + /// Represents a SettingElementCollection collection configuration element within the configuration. + /// + public class SettingElementCollection : ConfigurationElementCollection + { + /// + /// Gets the type of the . + /// + /// + /// The of this collection. + /// + public override ConfigurationElementCollectionType CollectionType + { + get { return ConfigurationElementCollectionType.BasicMap; } + } + + /// + /// Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class. + /// + /// + /// The name of the collection; otherwise, an empty string. The default is an empty string. + /// + protected override string ElementName + { + get { return "setting"; } + } + + /// + /// Gets or sets the + /// at the specified index within the collection. + /// + /// The index at which to get the specified object. + /// + /// The + /// at the specified index within the collection. + /// + public SettingElement this[int index] + { + get + { + return (SettingElement)BaseGet(index); + } + + set + { + if (this.BaseGet(index) != null) + { + this.BaseRemoveAt(index); + } + + this.BaseAdd(index, value); + } + } + + /// + /// Returns the setting element with the specified key. + /// + /// the key representing the element + /// the setting element + public new SettingElement this[string key] + { + get { return (SettingElement)BaseGet(key); } + } + + /// + /// Returns a value indicating whether the settings collection contains the + /// given object. + /// + /// The key to identify the setting. + /// True if the collection contains the key; otherwise false. + public bool ContainsKey(string key) + { + object[] keys = BaseGetAllKeys(); + + return keys.Any(obj => (string)obj == key); + } + + /// + /// Gets the element key for a specified PluginElement configuration element. + /// + /// + /// The ConfigurationElement + /// to return the key for. + /// + /// The element key for a specified PluginElement configuration element. + protected override object GetElementKey(ConfigurationElement element) + { + return ((SettingElement)element).Key; + } + + /// + /// Creates a new SettingElement configuration element. + /// + /// + /// A new SettingElement configuration element. + /// + protected override ConfigurationElement CreateNewElement() + { + return new SettingElement(); + } + } +} diff --git a/src/ImageProcessor.Web/Extensions/TypeInitializationExtensions.cs b/src/ImageProcessor.Web/Extensions/TypeInitializationExtensions.cs new file mode 100644 index 000000000..bd59d1b3b --- /dev/null +++ b/src/ImageProcessor.Web/Extensions/TypeInitializationExtensions.cs @@ -0,0 +1,180 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Extensions methods for for creating instances of types faster than +// using reflection. Modified from the original class at. +// +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Web.Extensions +{ + using System; + using System.Collections.Concurrent; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + + /// + /// Extensions methods for for creating instances of types faster than + /// using reflection. Modified from the original class at. + /// + /// + internal static class TypeInitializationExtensions + { + /// + /// Returns an instance of the on which the method is invoked. + /// + /// The type on which the method was invoked. + /// An instance of the . + public static object GetInstance(this Type type) + { + // This is about as quick as it gets. + return Activator.CreateInstance(type); + } + + /// + /// Returns an instance of the on which the method is invoked. + /// + /// The type of the argument to pass to the constructor. + /// The type on which the method was invoked. + /// The argument to pass to the constructor. + /// An instance of the given . + public static object GetInstance(this Type type, TArg argument) + { + return GetInstance(type, argument, null); + } + + /// + /// Returns an instance of the on which the method is invoked. + /// + /// The type of the first argument to pass to the constructor. + /// The type of the second argument to pass to the constructor. + /// The type on which the method was invoked. + /// The first argument to pass to the constructor. + /// The second argument to pass to the constructor. + /// An instance of the given . + public static object GetInstance(this Type type, TArg1 argument1, TArg2 argument2) + { + return GetInstance(type, argument1, argument2, null); + } + + /// + /// Returns an instance of the on which the method is invoked. + /// + /// The type of the first argument to pass to the constructor. + /// The type of the second argument to pass to the constructor. + /// The type of the third argument to pass to the constructor. + /// The type on which the method was invoked. + /// The first argument to pass to the constructor. + /// The second argument to pass to the constructor. + /// The third argument to pass to the constructor. + /// An instance of the given . + public static object GetInstance( + this Type type, + TArg1 argument1, + TArg2 argument2, + TArg3 argument3) + { + return InstanceCreationFactory + .CreateInstanceOf(type, argument1, argument2, argument3); + } + + /// + /// The instance creation factory for creating instances. + /// + /// The type of the first argument to pass to the constructor. + /// The type of the second argument to pass to the constructor. + /// The type of the third argument to pass to the constructor. + private static class InstanceCreationFactory + { + /// + /// This dictionary will hold a cache of object-creation functions, keyed by the Type to create: + /// + private static readonly ConcurrentDictionary> InstanceCreationMethods = new ConcurrentDictionary>(); + + /// + /// The create instance of. + /// + /// + /// The type. + /// + /// The first argument to pass to the constructor. + /// The second argument to pass to the constructor. + /// The third argument to pass to the constructor. + /// + /// The . + /// + public static object CreateInstanceOf(Type type, TArg1 arg1, TArg2 arg2, TArg3 arg3) + { + CacheInstanceCreationMethodIfRequired(type); + + return InstanceCreationMethods[type].Invoke(arg1, arg2, arg3); + } + + /// + /// Caches the instance creation method. + /// + /// + /// The who's constructor to cache. + /// + private static void CacheInstanceCreationMethodIfRequired(Type type) + { + // Bail out if we've already cached the instance creation method: + Func cached; + if (InstanceCreationMethods.TryGetValue(type, out cached)) + { + return; + } + + Type[] argumentTypes = { typeof(TArg1), typeof(TArg2), typeof(TArg3) }; + + // Get a collection of the constructor argument Types we've been given; ignore any + // arguments which are of the 'ignore this' Type: + Type[] constructorArgumentTypes = argumentTypes.Where(t => t != typeof(TypeToIgnore)).ToArray(); + + // Get the Constructor which matches the given argument Types: + ConstructorInfo constructor = type.GetConstructor( + BindingFlags.Instance | BindingFlags.Public, + null, + CallingConventions.HasThis, + constructorArgumentTypes, + new ParameterModifier[0]); + + // Get a set of Expressions representing the parameters which will be passed to the Func: + ParameterExpression[] lamdaParameterExpressions = + { + Expression.Parameter(typeof(TArg1), "param1"), + Expression.Parameter(typeof(TArg2), "param2"), + Expression.Parameter(typeof(TArg3), "param3") + }; + + // Get a set of Expressions representing the parameters which will be passed to the constructor: + ParameterExpression[] constructorParameterExpressions = + lamdaParameterExpressions.Take(constructorArgumentTypes.Length).ToArray(); + + // Get an Expression representing the constructor call, passing in the constructor parameters: + NewExpression constructorCallExpression = Expression.New(constructor, constructorParameterExpressions.Cast()); + + // Compile the Expression into a Func which takes three arguments and returns the constructed object: + Func constructorCallingLambda = + Expression.Lambda>( + constructorCallExpression, + lamdaParameterExpressions).Compile(); + + InstanceCreationMethods.TryAdd(type, constructorCallingLambda); + } + } + + /// + /// To allow for overloads with differing numbers of arguments, we flag arguments which should be + /// ignored by using this Type: + /// + private class TypeToIgnore + { + } + } +} diff --git a/src/ImageProcessor.Web/Helpers/TypePropertyHelpers.cs b/src/ImageProcessor.Web/Helpers/TypePropertyHelpers.cs new file mode 100644 index 000000000..0b3d850cb --- /dev/null +++ b/src/ImageProcessor.Web/Helpers/TypePropertyHelpers.cs @@ -0,0 +1,19 @@ +namespace ImageProcessor.Web.Extensions +{ + using System; + using System.Linq.Expressions; + + internal static class TypePropertyHelpers + { + public static string GetPropertyName(Expression> expression) + { + MemberExpression member = expression.Body as MemberExpression; + if (member != null) + { + return member.Member.Name; + } + + throw new ArgumentException("expression"); + } + } +} diff --git a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs index 5015c2ac4..caf79a5b6 100644 --- a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs +++ b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs @@ -3,34 +3,24 @@ // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // -// -// Processes any image requests within the web application. -// // -------------------------------------------------------------------------------------------------------------------- - namespace ImageProcessor.Web.HttpModules { - #region Using using System; using System.Collections.Generic; using System.IO; using System.Linq; - using System.Net; using System.Reflection; - using System.Security; - using System.Security.Permissions; - using System.Security.Principal; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; - using System.Web.Security; using ImageProcessor.Web.Caching; using ImageProcessor.Web.Configuration; + using ImageProcessor.Web.Extensions; using ImageProcessor.Web.Helpers; using ImageProcessor.Web.Services; - #endregion /// /// Processes any image requests within the web application. @@ -38,6 +28,7 @@ namespace ImageProcessor.Web.HttpModules public sealed class ImageProcessingModule : IHttpModule { #region Fields + /// /// The key for storing the response type of the current image. /// @@ -86,10 +77,14 @@ namespace ImageProcessor.Web.HttpModules /// private bool isDisposed; + /// + /// The image cache. + /// private IImageCache imageCache; #endregion #region Destructors + /// /// Finalizes an instance of the class. /// @@ -107,6 +102,7 @@ namespace ImageProcessor.Web.HttpModules // readability and maintainability. this.Dispose(false); } + #endregion /// @@ -132,6 +128,7 @@ namespace ImageProcessor.Web.HttpModules public static event ProcessQuerystringEventHandler OnProcessQuerystring; #region IHttpModule Members + /// /// Initializes a module and prepares it to handle requests. /// @@ -174,7 +171,9 @@ namespace ImageProcessor.Web.HttpModules /// /// Disposes the object and frees resources for the Garbage Collector. /// - /// If true, the object gets disposed. + /// + /// If true, the object gets disposed. + /// private void Dispose(bool disposing) { if (this.isDisposed) @@ -192,6 +191,7 @@ namespace ImageProcessor.Web.HttpModules // Note disposing is done. this.isDisposed = true; } + #endregion /// @@ -249,8 +249,12 @@ namespace ImageProcessor.Web.HttpModules /// /// Occurs just before ASP.NET send HttpHeaders to the client. /// - /// The source of the event. - /// An EventArgs that contains the event data. + /// + /// The source of the event. + /// + /// + /// An EventArgs that contains the event data. + /// private void ContextPreSendRequestHeaders(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; @@ -373,7 +377,8 @@ namespace ImageProcessor.Web.HttpModules } // Create a new cache to help process and cache the request. - this.imageCache = new DiskCache2(requestPath, fullPath, queryString); + this.imageCache = (IImageCache)ImageProcessorConfiguration.Instance + .ImageCache.GetInstance(requestPath, fullPath, queryString); // Is the file new or updated? bool isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync(); @@ -493,7 +498,7 @@ namespace ImageProcessor.Web.HttpModules cache.SetLastModifiedFromFileDependencies(); } - int maxDays = this.imageCache.MaxAge; + int maxDays = this.imageCache.MaxDays; cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(maxDays)); cache.SetMaxAge(new TimeSpan(maxDays, 0, 0, 0)); @@ -587,6 +592,7 @@ namespace ImageProcessor.Web.HttpModules // Return the file based service return services.FirstOrDefault(s => string.IsNullOrWhiteSpace(s.Prefix) && s.IsValidRequest(path)); } + #endregion } } \ No newline at end of file diff --git a/src/ImageProcessor.Web/ImageProcessor.Web.csproj b/src/ImageProcessor.Web/ImageProcessor.Web.csproj index 508bc4d43..642a6d634 100644 --- a/src/ImageProcessor.Web/ImageProcessor.Web.csproj +++ b/src/ImageProcessor.Web/ImageProcessor.Web.csproj @@ -46,9 +46,13 @@ - + + + + + diff --git a/src/ImageProcessor.Web/ImageProcessor.Web.csproj.DotSettings b/src/ImageProcessor.Web/ImageProcessor.Web.csproj.DotSettings new file mode 100644 index 000000000..ce2072422 --- /dev/null +++ b/src/ImageProcessor.Web/ImageProcessor.Web.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/TestWebsites/MVC/Test_Website_MVC.csproj b/src/TestWebsites/MVC/Test_Website_MVC.csproj index 6c6b551c4..3351fa9bf 100644 --- a/src/TestWebsites/MVC/Test_Website_MVC.csproj +++ b/src/TestWebsites/MVC/Test_Website_MVC.csproj @@ -152,6 +152,10 @@ + + {3c805e4c-d679-43f8-8c43-8909cdb4d4d7} + ImageProcessor.Web.AzureBlobCache + {55d08737-7d7e-4995-8892-bd9f944329e6} ImageProcessor.Web.PostProcessor diff --git a/src/TestWebsites/MVC/config/imageprocessor/cache.config b/src/TestWebsites/MVC/config/imageprocessor/cache.config index e4a9c5e9a..38bce1f29 100644 --- a/src/TestWebsites/MVC/config/imageprocessor/cache.config +++ b/src/TestWebsites/MVC/config/imageprocessor/cache.config @@ -1,3 +1,11 @@ - - + + + + + + + + + + From c0be36550644f257c14606d5af45e8a4045b980b Mon Sep 17 00:00:00 2001 From: James South Date: Tue, 17 Feb 2015 18:28:09 +0000 Subject: [PATCH 5/8] Initial wire up of Azure blob cache TODO: Error checking with container urls. Former-commit-id: 268e3adea98e2d485fea36565e8e804921b1144e Former-commit-id: b75f385b341d4d66c6438db0ebf6b3cded0039c7 --- .../ImageProcessor.Playground.csproj | 8 ++ src/ImageProcessor.Playground/Program.cs | 5 + .../AzureBlobCache.cs | 129 +++++++++++------- .../ImageProcessor.Web.AzureBlobCache.csproj | 4 +- .../PostProcessor.cs | 5 + .../Caching/CacheIndexer.cs | 17 ++- .../Configuration/ImageCacheSection.cs | 2 +- .../HttpModules/ImageProcessingModule.cs | 28 ---- src/TestWebsites/MVC/Views/Home/Index.cshtml | 18 +-- src/TestWebsites/MVC/Web.config | 4 +- 10 files changed, 125 insertions(+), 95 deletions(-) diff --git a/src/ImageProcessor.Playground/ImageProcessor.Playground.csproj b/src/ImageProcessor.Playground/ImageProcessor.Playground.csproj index 44d73c00b..0c659620c 100644 --- a/src/ImageProcessor.Playground/ImageProcessor.Playground.csproj +++ b/src/ImageProcessor.Playground/ImageProcessor.Playground.csproj @@ -55,6 +55,14 @@ + + {3c805e4c-d679-43f8-8c43-8909cdb4d4d7} + ImageProcessor.Web.AzureBlobCache + + + {d011a778-59c8-4bfa-a770-c350216bf161} + ImageProcessor.Web + {3B5DD734-FB7A-487D-8CE6-55E7AF9AEA7E} ImageProcessor diff --git a/src/ImageProcessor.Playground/Program.cs b/src/ImageProcessor.Playground/Program.cs index 937909347..e829732d8 100644 --- a/src/ImageProcessor.Playground/Program.cs +++ b/src/ImageProcessor.Playground/Program.cs @@ -24,6 +24,7 @@ namespace ImageProcessor.PlayGround using ImageProcessor.Imaging.Filters.Photo; using ImageProcessor.Imaging.Formats; using ImageProcessor.Processors; + using ImageProcessor.Web.Caching; /// /// The program. @@ -38,6 +39,10 @@ namespace ImageProcessor.PlayGround /// public static void Main(string[] args) { + var x = typeof(AzureBlobCache); + Console.WriteLine(x.AssemblyQualifiedName); + Console.ReadLine(); + string path = new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath; // ReSharper disable once AssignNullToNotNullAttribute diff --git a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs index 3820f421a..72c23c5ea 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs +++ b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs @@ -1,19 +1,17 @@ -namespace ImageProcessor.Web.AzureBlobCache +namespace ImageProcessor.Web.Caching { using System; using System.Collections.Generic; - using System.Configuration; using System.Globalization; using System.IO; using System.Linq; + using System.Net; using System.Threading.Tasks; using System.Web; - using ImageProcessor.Web.Caching; using ImageProcessor.Web.Extensions; using ImageProcessor.Web.Helpers; - using Microsoft.WindowsAzure; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; @@ -36,7 +34,9 @@ private CloudBlobContainer cloudSourceBlobContainer; - private string cachedContainerRoot; + private string cachedCDNRoot; + + private string cachedRewritePath; /// /// The physical cached path. @@ -60,7 +60,7 @@ this.cloudCachedBlobContainer = this.cloudCachedBlobClient.GetContainerReference(this.Settings["CachedBlobContainer"]); this.cloudSourceBlobContainer = this.cloudSourceBlobClient.GetContainerReference(this.Settings["SourceBlobContainer"]); - this.cachedContainerRoot = this.Settings["CachedContainerRoot"]; + this.cachedCDNRoot = this.Settings["CachedCDNRoot"]; } public override int MaxDays @@ -78,15 +78,36 @@ // 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)); - this.CachedPath = Path.Combine(this.cachedContainerRoot, pathFromKey, cachedFileName).Replace(@"\", "/"); + this.CachedPath = Path.Combine(this.cloudCachedBlobContainer.Uri.ToString(), pathFromKey, cachedFileName).Replace(@"\", "/"); + this.cachedRewritePath = Path.Combine(this.cachedCDNRoot, this.cloudCachedBlobContainer.Name, pathFromKey, cachedFileName).Replace(@"\", "/"); bool isUpdated = false; CachedImage cachedImage = CacheIndexer.GetValue(this.CachedPath); + if (new Uri(this.CachedPath).IsFile) + { + 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) { - ICloudBlob blockBlob = - await this.cloudCachedBlobContainer.GetBlobReferenceFromServerAsync(this.RequestPath); + string blobPath = this.CachedPath.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1); + CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(blobPath); if (await blockBlob.ExistsAsync()) { @@ -106,21 +127,21 @@ CacheIndexer.Add(cachedImage); } } + } - if (cachedImage == null) + 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)) { - // Nothing in the cache so we should return true. + CacheIndexer.Remove(this.CachedPath); 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; @@ -128,7 +149,8 @@ public override async Task AddImageToCacheAsync(Stream stream) { - CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(this.CachedPath); + string blobPath = this.CachedPath.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1); + CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(blobPath); await blockBlob.UploadFromStreamAsync(stream); } @@ -137,7 +159,7 @@ Uri uri = new Uri(this.CachedPath); string path = uri.GetLeftPart(UriPartial.Path); string directory = path.Substring(0, path.LastIndexOf('/')); - string parent = directory.Substring(0, path.LastIndexOf('/')); + string parent = directory.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1, path.LastIndexOf('/')); BlobContinuationToken continuationToken = null; CloudBlobDirectory directoryBlob = this.cloudCachedBlobContainer.GetDirectoryReference(parent); @@ -163,12 +185,10 @@ { break; } - else - { - // Remove from the cache and delete each CachedImage. - CacheIndexer.Remove(blob.Name); - await blob.DeleteAsync(); - } + + // Remove from the cache and delete each CachedImage. + CacheIndexer.Remove(blob.Name); + await blob.DeleteAsync(); } } @@ -180,8 +200,24 @@ { if (new Uri(this.RequestPath).IsFile) { - ICloudBlob blockBlob = await this.cloudSourceBlobContainer - .GetBlobReferenceFromServerAsync(this.RequestPath); + // 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); + } + } + else + { + string blobPath = this.CachedPath.Substring(this.cloudSourceBlobContainer.Uri.ToString().Length + 1); + CloudBlockBlob blockBlob = this.cloudSourceBlobContainer.GetBlockBlobReference(blobPath); if (await blockBlob.ExistsAsync()) { @@ -190,24 +226,11 @@ if (blockBlob.Properties.LastModified.HasValue) { - string creation = blockBlob.Properties.LastModified.Value.UtcDateTime.ToString(CultureInfo.InvariantCulture); - string length = blockBlob.Properties.Length.ToString(CultureInfo.InvariantCulture); - streamHash = string.Format("{0}{1}", creation, length); - } - } - else - { - // 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(); + string creation = blockBlob.Properties + .LastModified.Value.UtcDateTime + .ToString(CultureInfo.InvariantCulture); - // 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); + string length = blockBlob.Properties.Length.ToString(CultureInfo.InvariantCulture); streamHash = string.Format("{0}{1}", creation, length); } } @@ -234,8 +257,16 @@ public override void RewritePath(HttpContext context) { - // The cached file is valid so just rewrite the path. - context.RewritePath(this.CachedPath, false); + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.cachedRewritePath); + request.Method = "HEAD"; + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + HttpStatusCode responseCode = response.StatusCode; + context.Response.Redirect( + responseCode == HttpStatusCode.NotFound ? this.CachedPath : this.cachedRewritePath, + false); + } } } } diff --git a/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj b/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj index f2b15b3ab..6e57a2a38 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj +++ b/src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj @@ -7,8 +7,8 @@ {3C805E4C-D679-43F8-8C43-8909CDB4D4D7} Library Properties - ImageProcessor.Web.AzureBlobCache - ImageProcessor.Web.AzureBlobCache + ImageProcessor.Web.Caching + ImageProcessor.Web.Caching.AzureBlobCache v4.5 512 ..\ diff --git a/src/ImageProcessor.Web.PostProcessor/PostProcessor.cs b/src/ImageProcessor.Web.PostProcessor/PostProcessor.cs index 9d3a2be70..4144aea5b 100644 --- a/src/ImageProcessor.Web.PostProcessor/PostProcessor.cs +++ b/src/ImageProcessor.Web.PostProcessor/PostProcessor.cs @@ -34,6 +34,11 @@ namespace ImageProcessor.Web.PostProcessor /// public static async Task PostProcessImageAsync(string sourceFile) { + if (!new Uri(sourceFile).IsFile) + { + return; + } + string targetFile = Path.GetTempFileName(); PostProcessingResultEventArgs result = await RunProcess(sourceFile, targetFile); diff --git a/src/ImageProcessor.Web/Caching/CacheIndexer.cs b/src/ImageProcessor.Web/Caching/CacheIndexer.cs index 8a7786eb3..39654e83a 100644 --- a/src/ImageProcessor.Web/Caching/CacheIndexer.cs +++ b/src/ImageProcessor.Web/Caching/CacheIndexer.cs @@ -10,6 +10,7 @@ namespace ImageProcessor.Web.Caching { + using System; using System.Collections.Generic; using System.IO; using System.Runtime.Caching; @@ -64,11 +65,19 @@ namespace ImageProcessor.Web.Caching /// public static CachedImage Add(CachedImage cachedImage) { - // Add the CachedImage. - CacheItemPolicy policy = new CacheItemPolicy(); - policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List { cachedImage.Path })); + if (new Uri(cachedImage.Path).IsFile) + { + // Add the CachedImage. + CacheItemPolicy policy = new CacheItemPolicy(); + policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List { cachedImage.Path })); + + MemCache.AddItem(Path.GetFileNameWithoutExtension(cachedImage.Key), cachedImage, policy); + } + else + { + MemCache.AddItem(Path.GetFileNameWithoutExtension(cachedImage.Key), cachedImage); + } - MemCache.AddItem(Path.GetFileNameWithoutExtension(cachedImage.Key), cachedImage, policy); return cachedImage; } #endregion diff --git a/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs b/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs index 2aa05cc04..7847978f5 100644 --- a/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs +++ b/src/ImageProcessor.Web/Configuration/ImageCacheSection.cs @@ -59,7 +59,7 @@ namespace ImageProcessor.Web.Configuration /// The cache configuration section from the current application configuration. public static ImageCacheSection GetConfiguration() { - ImageCacheSection imageCacheSection = ConfigurationManager.GetSection("imageProcessor/cache") as ImageCacheSection; + ImageCacheSection imageCacheSection = ConfigurationManager.GetSection("imageProcessor/caching") as ImageCacheSection; if (imageCacheSection != null) { diff --git a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs index caf79a5b6..160d3d48e 100644 --- a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs +++ b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs @@ -425,34 +425,6 @@ namespace ImageProcessor.Web.HttpModules } } - // 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 (!string.IsNullOrEmpty(mimetype)) - { - context.Items[CachedResponseTypeKey] = mimetype; - } - } - - if (context.Items[CachedResponseFileDependency] == null) - { - 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); } diff --git a/src/TestWebsites/MVC/Views/Home/Index.cshtml b/src/TestWebsites/MVC/Views/Home/Index.cshtml index bceabd8ff..39dc3930e 100644 --- a/src/TestWebsites/MVC/Views/Home/Index.cshtml +++ b/src/TestWebsites/MVC/Views/Home/Index.cshtml @@ -7,24 +7,24 @@

Resized

- -

Foreign language test.

+ + @*

Foreign language test.

Strange name

- + *@
-
+ @*

Cropped

Cropped Percent

-
+
*@
-
+ @*

Reside Pad

@@ -199,9 +199,9 @@
-
+
*@ -
+@*

Color Profiles

@@ -223,4 +223,4 @@
-
\ No newline at end of file +
*@ \ No newline at end of file diff --git a/src/TestWebsites/MVC/Web.config b/src/TestWebsites/MVC/Web.config index eb09cc6d7..136c9c7fa 100644 --- a/src/TestWebsites/MVC/Web.config +++ b/src/TestWebsites/MVC/Web.config @@ -9,12 +9,12 @@
-
+
- + From 50de09e0406dfa8855ac97c0ecc5605a0d816c04 Mon Sep 17 00:00:00 2001 From: James South Date: Wed, 18 Feb 2015 09:29:22 +0000 Subject: [PATCH 6/8] Add correct mimetype and expires to blob Former-commit-id: a64b001ebf5b6c4c49740688f8c88743665990fe Former-commit-id: 143dbc1f537e0c127dc61fc07e29590560b3f79c --- .../AzureBlobCache.cs | 13 +++++++---- src/ImageProcessor.Web/Caching/DiskCache.cs | 4 ++-- src/ImageProcessor.Web/Caching/IImageCache.cs | 2 +- .../Caching/ImageCacheBase.cs | 2 +- .../HttpModules/ImageProcessingModule.cs | 23 ++++++++++++++----- src/TestWebsites/MVC/Views/Home/Index.cshtml | 2 +- .../MVC/config/imageprocessor/cache.config | 2 +- 7 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs index 72c23c5ea..dc6e1a74c 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs +++ b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs @@ -46,7 +46,7 @@ public AzureBlobCache(string requestPath, string fullPath, string querystring) : base(requestPath, fullPath, querystring) { - this.maxDays = Convert.ToInt32(this.Settings["MaxAge"]); + this.maxDays = Convert.ToInt32(this.Settings["MaxDays"]); // Retrieve storage accounts from connection string. this.cloudCachedStorageAccount = CloudStorageAccount.Parse(this.Settings["CachedStorageAccount"]); @@ -147,19 +147,24 @@ return isUpdated; } - public override async Task AddImageToCacheAsync(Stream stream) + public override async Task AddImageToCacheAsync(Stream stream, string contentType) { string blobPath = this.CachedPath.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1); CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(blobPath); + await blockBlob.UploadFromStreamAsync(stream); + + blockBlob.Properties.ContentType = contentType; + blockBlob.Properties.CacheControl = string.Format("public, max-age={0}", this.MaxDays * 86400); + await blockBlob.SetPropertiesAsync(); } public override async Task TrimCacheAsync() { Uri uri = new Uri(this.CachedPath); - string path = uri.GetLeftPart(UriPartial.Path); + string path = uri.GetLeftPart(UriPartial.Path).Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1); string directory = path.Substring(0, path.LastIndexOf('/')); - string parent = directory.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1, path.LastIndexOf('/')); + string parent = directory.Substring(path.LastIndexOf('/')); BlobContinuationToken continuationToken = null; CloudBlobDirectory directoryBlob = this.cloudCachedBlobContainer.GetDirectoryReference(parent); diff --git a/src/ImageProcessor.Web/Caching/DiskCache.cs b/src/ImageProcessor.Web/Caching/DiskCache.cs index e0e2ec555..451183526 100644 --- a/src/ImageProcessor.Web/Caching/DiskCache.cs +++ b/src/ImageProcessor.Web/Caching/DiskCache.cs @@ -61,7 +61,7 @@ public DiskCache(string requestPath, string fullPath, string querystring) : base(requestPath, fullPath, querystring) { - this.maxDays = Convert.ToInt32(this.Settings["MaxAge"]); + this.maxDays = Convert.ToInt32(this.Settings["MaxDays"]); string virtualPath = this.Settings["VirtualCachePath"]; if (!virtualPath.IsValidVirtualPathName()) @@ -138,7 +138,7 @@ return isUpdated; } - public override async Task AddImageToCacheAsync(Stream stream) + public override async Task AddImageToCacheAsync(Stream stream, string contentType) { // ReSharper disable once AssignNullToNotNullAttribute DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.CachedPath)); diff --git a/src/ImageProcessor.Web/Caching/IImageCache.cs b/src/ImageProcessor.Web/Caching/IImageCache.cs index 3b2d1d337..976d47012 100644 --- a/src/ImageProcessor.Web/Caching/IImageCache.cs +++ b/src/ImageProcessor.Web/Caching/IImageCache.cs @@ -19,7 +19,7 @@ namespace ImageProcessor.Web.Caching Task IsNewOrUpdatedAsync(); - Task AddImageToCacheAsync(Stream stream); + Task AddImageToCacheAsync(Stream stream, string contentType); Task TrimCacheAsync(); diff --git a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs index 84517be32..b49602501 100644 --- a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs +++ b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs @@ -60,7 +60,7 @@ public abstract Task IsNewOrUpdatedAsync(); - public abstract Task AddImageToCacheAsync(Stream stream); + public abstract Task AddImageToCacheAsync(Stream stream, string contentType); public abstract Task TrimCacheAsync(); diff --git a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs index 160d3d48e..d78bbc485 100644 --- a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs +++ b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs @@ -404,19 +404,30 @@ namespace ImageProcessor.Web.HttpModules memoryStream.Position = 0; // Add to the cache. - await this.imageCache.AddImageToCacheAsync(memoryStream); + await this.imageCache.AddImageToCacheAsync(memoryStream, 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; + bool isFileCached = new Uri(cachedPath).IsFile; + 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 }; + if (isFileCached) + { + // 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] = Path.GetFileName(requestPath) == requestPath + ? null + : new List { requestPath }; + } } - else + else if (isFileCached) { context.Items[CachedResponseFileDependency] = new List { cachedPath }; } diff --git a/src/TestWebsites/MVC/Views/Home/Index.cshtml b/src/TestWebsites/MVC/Views/Home/Index.cshtml index 39dc3930e..f010a23d9 100644 --- a/src/TestWebsites/MVC/Views/Home/Index.cshtml +++ b/src/TestWebsites/MVC/Views/Home/Index.cshtml @@ -7,7 +7,7 @@

Resized

- + @*

Foreign language test.

diff --git a/src/TestWebsites/MVC/config/imageprocessor/cache.config b/src/TestWebsites/MVC/config/imageprocessor/cache.config index 38bce1f29..5dee07828 100644 --- a/src/TestWebsites/MVC/config/imageprocessor/cache.config +++ b/src/TestWebsites/MVC/config/imageprocessor/cache.config @@ -2,7 +2,7 @@ - + From e386b479c6c81612a569a2941591ac6908d90b24 Mon Sep 17 00:00:00 2001 From: James South Date: Wed, 18 Feb 2015 10:46:01 +0000 Subject: [PATCH 7/8] Adding code comments Former-commit-id: 2a51566a5c79a24bda73701a439d91b1492b0f59 Former-commit-id: 2d4fff12db92bc6a959fb94a5f2b3a4e551fe0f0 --- .../AzureBlobCache.cs | 115 ++++++++++++++---- src/ImageProcessor.Web/Caching/DiskCache.cs | 55 ++++++++- src/ImageProcessor.Web/Caching/IImageCache.cs | 58 ++++++++- .../Caching/ImageCacheBase.cs | 62 +++++++++- .../Services/IImageService.cs | 4 +- src/ImageProcessor.Web/Settings.StyleCop | 1 + 6 files changed, 256 insertions(+), 39 deletions(-) diff --git a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs index dc6e1a74c..6bda004f3 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs +++ b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs @@ -1,4 +1,15 @@ -namespace ImageProcessor.Web.Caching +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Provides an implementation that uses Azure blob storage. +// The cache is self healing and cleaning. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Web.Caching { using System; using System.Collections.Generic; @@ -15,54 +26,72 @@ using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; + /// + /// Provides an implementation that uses Azure blob storage. + /// The cache is self healing and cleaning. + /// public class AzureBlobCache : ImageCacheBase { /// - /// The max age. + /// The maximum number of days to store the image. /// private readonly int maxDays; - private CloudStorageAccount cloudCachedStorageAccount; - - private CloudStorageAccount cloudSourceStorageAccount; - - private CloudBlobClient cloudCachedBlobClient; - - private CloudBlobClient cloudSourceBlobClient; - - private CloudBlobContainer cloudCachedBlobContainer; + /// + /// The cloud cached blob container. + /// + private readonly CloudBlobContainer cloudCachedBlobContainer; - private CloudBlobContainer cloudSourceBlobContainer; + /// + /// The cloud source blob container. + /// + private readonly CloudBlobContainer cloudSourceBlobContainer; - private string cachedCDNRoot; + /// + /// The cached root url for a content delivery network. + /// + private readonly string cachedCdnRoot; + /// + /// The cached rewrite path. + /// private string cachedRewritePath; /// - /// The physical cached path. + /// Initializes a new instance of the class. /// - private string physicalCachedPath; - + /// + /// The request path for the image. + /// + /// + /// The full path for the image. + /// + /// + /// The querystring containing instructions. + /// public AzureBlobCache(string requestPath, string fullPath, string querystring) : base(requestPath, fullPath, querystring) { this.maxDays = Convert.ToInt32(this.Settings["MaxDays"]); // Retrieve storage accounts from connection string. - this.cloudCachedStorageAccount = CloudStorageAccount.Parse(this.Settings["CachedStorageAccount"]); - this.cloudSourceStorageAccount = CloudStorageAccount.Parse(this.Settings["SourceStorageAccount"]); + CloudStorageAccount cloudCachedStorageAccount = CloudStorageAccount.Parse(this.Settings["CachedStorageAccount"]); + CloudStorageAccount cloudSourceStorageAccount = CloudStorageAccount.Parse(this.Settings["SourceStorageAccount"]); // Create the blob clients. - this.cloudCachedBlobClient = this.cloudCachedStorageAccount.CreateCloudBlobClient(); - this.cloudSourceBlobClient = this.cloudSourceStorageAccount.CreateCloudBlobClient(); + CloudBlobClient cloudCachedBlobClient = cloudCachedStorageAccount.CreateCloudBlobClient(); + CloudBlobClient cloudSourceBlobClient = cloudSourceStorageAccount.CreateCloudBlobClient(); // Retrieve references to a previously created containers. - this.cloudCachedBlobContainer = this.cloudCachedBlobClient.GetContainerReference(this.Settings["CachedBlobContainer"]); - this.cloudSourceBlobContainer = this.cloudSourceBlobClient.GetContainerReference(this.Settings["SourceBlobContainer"]); + this.cloudCachedBlobContainer = cloudCachedBlobClient.GetContainerReference(this.Settings["CachedBlobContainer"]); + this.cloudSourceBlobContainer = cloudSourceBlobClient.GetContainerReference(this.Settings["SourceBlobContainer"]); - this.cachedCDNRoot = this.Settings["CachedCDNRoot"]; + this.cachedCdnRoot = this.Settings["CachedCDNRoot"]; } + /// + /// Gets the maximum number of days to store the image. + /// public override int MaxDays { get @@ -71,6 +100,12 @@ } } + /// + /// Gets a value indicating whether the image is new or updated in an asynchronous manner. + /// + /// + /// The asynchronous returning the value. + /// public override async Task IsNewOrUpdatedAsync() { string cachedFileName = await this.CreateCachedFileName(); @@ -79,7 +114,7 @@ // That gives us massive scope to store millions of files. string pathFromKey = string.Join("\\", cachedFileName.ToCharArray().Take(6)); this.CachedPath = Path.Combine(this.cloudCachedBlobContainer.Uri.ToString(), pathFromKey, cachedFileName).Replace(@"\", "/"); - this.cachedRewritePath = Path.Combine(this.cachedCDNRoot, this.cloudCachedBlobContainer.Name, pathFromKey, cachedFileName).Replace(@"\", "/"); + this.cachedRewritePath = Path.Combine(this.cachedCdnRoot, this.cloudCachedBlobContainer.Name, pathFromKey, cachedFileName).Replace(@"\", "/"); bool isUpdated = false; CachedImage cachedImage = CacheIndexer.GetValue(this.CachedPath); @@ -147,18 +182,36 @@ 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) { string blobPath = this.CachedPath.Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1); CloudBlockBlob blockBlob = this.cloudCachedBlobContainer.GetBlockBlobReference(blobPath); await blockBlob.UploadFromStreamAsync(stream); - + blockBlob.Properties.ContentType = contentType; blockBlob.Properties.CacheControl = string.Format("public, max-age={0}", this.MaxDays * 86400); await blockBlob.SetPropertiesAsync(); } + /// + /// Trims the cache of any expired items in an asynchronous manner. + /// + /// + /// The asynchronous representing an asynchronous operation. + /// public override async Task TrimCacheAsync() { Uri uri = new Uri(this.CachedPath); @@ -197,6 +250,12 @@ } } + /// + /// Gets a string identifying the cached file name. + /// + /// + /// The asynchronous returning the value. + /// public override async Task CreateCachedFileName() { string streamHash = string.Empty; @@ -260,6 +319,12 @@ return cachedFileName; } + /// + /// Rewrites the path to point to the cached image. + /// + /// + /// The encapsulating all information about the request. + /// public override void RewritePath(HttpContext context) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.cachedRewritePath); diff --git a/src/ImageProcessor.Web/Caching/DiskCache.cs b/src/ImageProcessor.Web/Caching/DiskCache.cs index 451183526..e02cd4e24 100644 --- a/src/ImageProcessor.Web/Caching/DiskCache.cs +++ b/src/ImageProcessor.Web/Caching/DiskCache.cs @@ -1,4 +1,15 @@ -namespace ImageProcessor.Web.Caching +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; @@ -9,9 +20,12 @@ using System.Web; using System.Web.Hosting; - using ImageProcessor.Web.Configuration; using ImageProcessor.Web.Extensions; + /// + /// Provides an implementation that is file system based. + /// The cache is self healing and cleaning. + /// public class DiskCache : ImageCacheBase { /// @@ -27,7 +41,7 @@ private const int MaxFilesCount = 100; /// - /// The max age. + /// The maximum number of days to store the image. /// private readonly int maxDays; @@ -42,7 +56,7 @@ private readonly string absoluteCachePath; /// - /// The virtual cached path to the cached file. + /// The virtual path to the cached file. /// private string virtualCachedFilePath; @@ -75,8 +89,7 @@ } /// - /// The maximum number of days to cache files on the system for. - /// TODO: Shift the getter source to proper config. + /// Gets the maximum number of days to store the image. /// public override int MaxDays { @@ -86,6 +99,12 @@ } } + /// + /// 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(); @@ -138,6 +157,18 @@ 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 @@ -153,6 +184,12 @@ } } + /// + /// 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); @@ -198,6 +235,12 @@ } } + /// + /// 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. diff --git a/src/ImageProcessor.Web/Caching/IImageCache.cs b/src/ImageProcessor.Web/Caching/IImageCache.cs index 976d47012..9c9214824 100644 --- a/src/ImageProcessor.Web/Caching/IImageCache.cs +++ b/src/ImageProcessor.Web/Caching/IImageCache.cs @@ -1,4 +1,13 @@ - +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Defines properties and methods for allowing caching of images to different sources. +// +// -------------------------------------------------------------------------------------------------------------------- + namespace ImageProcessor.Web.Caching { using System.Collections.Generic; @@ -6,6 +15,9 @@ namespace ImageProcessor.Web.Caching using System.Threading.Tasks; using System.Web; + /// + /// Defines properties and methods for allowing caching of images to different sources. + /// public interface IImageCache { /// @@ -13,20 +25,60 @@ namespace ImageProcessor.Web.Caching /// Dictionary Settings { get; set; } + /// + /// Gets the path to the cached image. + /// string CachedPath { get; } + /// + /// Gets the maximum number of days to store the image. + /// int MaxDays { get; } + /// + /// Gets a value indicating whether the image is new or updated in an asynchronous manner. + /// + /// + /// The asynchronous returning the value. + /// Task IsNewOrUpdatedAsync(); + /// + /// 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. + /// Task AddImageToCacheAsync(Stream stream, string contentType); + /// + /// Trims the cache of any expired items in an asynchronous manner. + /// + /// + /// The asynchronous representing an asynchronous operation. + /// Task TrimCacheAsync(); + /// + /// Gets a string identifying the cached file name. + /// + /// + /// The asynchronous returning the value. + /// Task CreateCachedFileName(); + /// + /// Rewrites the path to point to the cached image. + /// + /// + /// The encapsulating all information about the request. + /// void RewritePath(HttpContext context); - - //void SetHeaders(HttpContext context); } } diff --git a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs index b49602501..4a036a2f6 100644 --- a/src/ImageProcessor.Web/Caching/ImageCacheBase.cs +++ b/src/ImageProcessor.Web/Caching/ImageCacheBase.cs @@ -1,10 +1,20 @@ -namespace ImageProcessor.Web.Caching +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// The image cache base provides methods for implementing the interface. +// It is recommended that any implementations inherit from this class. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Web.Caching { using System; using System.Collections.Generic; using System.Globalization; using System.IO; - using System.Reflection; using System.Threading.Tasks; using System.Web; @@ -12,6 +22,10 @@ using ImageProcessor.Web.Extensions; using ImageProcessor.Web.Helpers; + /// + /// The image cache base provides methods for implementing the interface. + /// It is recommended that any implementations inherit from this class. + /// public abstract class ImageCacheBase : IImageCache { /// @@ -54,16 +68,52 @@ /// public Dictionary Settings { get; set; } - public string CachedPath { get; protected set; } + /// + /// Gets or sets the path to the cached image. + /// + public string CachedPath { get; set; } + /// + /// Gets the maximum number of days to store the image. + /// public abstract int MaxDays { get; } + /// + /// Gets a value indicating whether the image is new or updated in an asynchronous manner. + /// + /// + /// The . + /// public abstract Task IsNewOrUpdatedAsync(); + /// + /// 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 abstract Task AddImageToCacheAsync(Stream stream, string contentType); + /// + /// Trims the cache of any expired items in an asynchronous manner. + /// + /// + /// The asynchronous representing an asynchronous operation. + /// public abstract Task TrimCacheAsync(); + /// + /// Gets a string identifying the cached file name. + /// + /// + /// The asynchronous returning the value. + /// public virtual Task CreateCachedFileName() { string streamHash = string.Empty; @@ -106,6 +156,12 @@ return Task.FromResult(cachedFileName); } + /// + /// Rewrites the path to point to the cached image. + /// + /// + /// The encapsulating all information about the request. + /// public abstract void RewritePath(HttpContext context); /// diff --git a/src/ImageProcessor.Web/Services/IImageService.cs b/src/ImageProcessor.Web/Services/IImageService.cs index dfdcf0cdc..c6c39f80f 100644 --- a/src/ImageProcessor.Web/Services/IImageService.cs +++ b/src/ImageProcessor.Web/Services/IImageService.cs @@ -4,7 +4,7 @@ // Licensed under the Apache License, Version 2.0. // // -// Defines properties and methods for allowing retrieval of image from different sources. +// Defines properties and methods for allowing retrieval of images from different sources. // // -------------------------------------------------------------------------------------------------------------------- @@ -15,7 +15,7 @@ namespace ImageProcessor.Web.Services using System.Threading.Tasks; /// - /// Defines properties and methods for allowing retrieval of image from different sources. + /// Defines properties and methods for allowing retrieval of images from different sources. /// public interface IImageService { diff --git a/src/ImageProcessor.Web/Settings.StyleCop b/src/ImageProcessor.Web/Settings.StyleCop index 09dd1de03..311872ed4 100644 --- a/src/ImageProcessor.Web/Settings.StyleCop +++ b/src/ImageProcessor.Web/Settings.StyleCop @@ -1,6 +1,7 @@ + cdn dllimport From a49514bdd46b9cdf06ccc1fb114a42e1370ac982 Mon Sep 17 00:00:00 2001 From: James South Date: Thu, 19 Feb 2015 14:02:26 +0000 Subject: [PATCH 8/8] Fixing redirect and cleanup. Former-commit-id: 32e418b0f8b0b3b5a48dde36cc2f0ca52b8d3713 Former-commit-id: 31ab4e4c9d803625819d2646641c204460f69ed5 --- build/build.xml | 6 +- .../AzureBlobCache.cs | 12 ++- .../Properties/AssemblyInfo.cs | 4 +- .../README.md | 8 +- .../Configuration/Resources/security.config | 1 + .../Helpers/ImageHelpers.cs | 24 ------ src/ImageProcessor.Web/Helpers/RemoteFile.cs | 11 ++- .../HttpModules/ImageProcessingModule.cs | 82 +++++++++++-------- .../Services/RemoteImageService.cs | 5 +- src/ImageProcessor.sln.DotSettings | 1 + src/ImageProcessor/ImageFactory.cs | 6 +- .../Imaging/Formats/FormatBase.cs | 1 - src/TestWebsites/MVC/Global.asax.cs | 18 ++-- src/TestWebsites/MVC/Views/Home/Index.cshtml | 3 +- .../MVC/config/imageprocessor/security.config | 2 + 15 files changed, 94 insertions(+), 90 deletions(-) diff --git a/build/build.xml b/build/build.xml index c2cf5d0bd..eddcb6daa 100644 --- a/build/build.xml +++ b/build/build.xml @@ -13,7 +13,7 @@ ImageProcessor Web - 4.1.5.0 + 4.2.0.0 ..\src\ImageProcessor.Web ImageProcessor.Web.csproj @@ -24,7 +24,7 @@ ImageProcessor Web PostProcessor - 1.0.1.0 + 1.0.2.0 ..\src\ImageProcessor.Web.PostProcessor ImageProcessor.Web.PostProcessor.csproj @@ -35,7 +35,7 @@ ImageProcessor Web.config sample - 2.1.1.0 + 2.2.0.0 ImageProcessor.Web.Config.nuspec diff --git a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs index 6bda004f3..a206938f8 100644 --- a/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs +++ b/src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs @@ -17,6 +17,7 @@ namespace ImageProcessor.Web.Caching using System.IO; using System.Linq; using System.Net; + using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; @@ -217,16 +218,16 @@ namespace ImageProcessor.Web.Caching Uri uri = new Uri(this.CachedPath); string path = uri.GetLeftPart(UriPartial.Path).Substring(this.cloudCachedBlobContainer.Uri.ToString().Length + 1); string directory = path.Substring(0, path.LastIndexOf('/')); - string parent = directory.Substring(path.LastIndexOf('/')); + string parent = directory.Substring(0, directory.LastIndexOf('/')); BlobContinuationToken continuationToken = null; - CloudBlobDirectory directoryBlob = this.cloudCachedBlobContainer.GetDirectoryReference(parent); List results = new List(); // Loop through the all the files in a non blocking fashion. do { - BlobResultSegment response = await directoryBlob.ListBlobsSegmentedAsync(continuationToken); + BlobResultSegment response = await this.cloudCachedBlobContainer + .ListBlobsSegmentedAsync(parent, true, BlobListingDetails.Metadata, 5000, continuationToken, null, null); continuationToken = response.ContinuationToken; results.AddRange(response.Results); } @@ -280,7 +281,10 @@ namespace ImageProcessor.Web.Caching } else { - string blobPath = this.CachedPath.Substring(this.cloudSourceBlobContainer.Uri.ToString().Length + 1); + Regex regex = new Regex("^http(s)?://"); + string container = regex.Replace(this.cloudSourceBlobContainer.Uri.ToString(), string.Empty); + string blobPath = regex.Replace(this.RequestPath, string.Empty); + blobPath = blobPath.Replace(container, string.Empty).TrimStart('/'); CloudBlockBlob blockBlob = this.cloudSourceBlobContainer.GetBlockBlobReference(blobPath); if (await blockBlob.ExistsAsync()) diff --git a/src/ImageProcessor.Web.PostProcessor/Properties/AssemblyInfo.cs b/src/ImageProcessor.Web.PostProcessor/Properties/AssemblyInfo.cs index d0ffe6879..308ac564f 100644 --- a/src/ImageProcessor.Web.PostProcessor/Properties/AssemblyInfo.cs +++ b/src/ImageProcessor.Web.PostProcessor/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // 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.1.0")] -[assembly: AssemblyVersion("1.0.1.0")] -[assembly: AssemblyFileVersion("1.0.1.0")] +[assembly: AssemblyVersion("1.0.2.0")] +[assembly: AssemblyFileVersion("1.0.2.0")] diff --git a/src/ImageProcessor.Web.PostProcessor/README.md b/src/ImageProcessor.Web.PostProcessor/README.md index 05c331aeb..c91c7eb72 100644 --- a/src/ImageProcessor.Web.PostProcessor/README.md +++ b/src/ImageProcessor.Web.PostProcessor/README.md @@ -1,13 +1,13 @@ #Resource locations ###gifsicle -http://www.lcdf.org/gifsicle/ +[http://www.lcdf.org/gifsicle/](http://www.lcdf.org/gifsicle/) ###jpegtran -http://jpegclub.org/jpegtran/ +[http://jpegclub.org/jpegtran/](http://jpegclub.org/jpegtran/) ###optipng -http://optipng.sourceforge.net/ +[http://optipng.sourceforge.net/](http://optipng.sourceforge.net/) ###pngout -http://advsys.net/ken/utils.htm \ No newline at end of file +[http://advsys.net/ken/utils.htm](http://advsys.net/ken/utils.htm) \ No newline at end of file diff --git a/src/ImageProcessor.Web/Configuration/Resources/security.config b/src/ImageProcessor.Web/Configuration/Resources/security.config index 86e92d567..1160ce63b 100644 --- a/src/ImageProcessor.Web/Configuration/Resources/security.config +++ b/src/ImageProcessor.Web/Configuration/Resources/security.config @@ -5,6 +5,7 @@ + diff --git a/src/ImageProcessor.Web/Helpers/ImageHelpers.cs b/src/ImageProcessor.Web/Helpers/ImageHelpers.cs index 22d73d537..d53adf88b 100644 --- a/src/ImageProcessor.Web/Helpers/ImageHelpers.cs +++ b/src/ImageProcessor.Web/Helpers/ImageHelpers.cs @@ -104,30 +104,6 @@ namespace ImageProcessor.Web.Helpers return string.Empty; } - /// - /// Get the correct mime-type for the given string input. - /// - /// - /// The path to the cached image. - /// - /// - /// The matching the correct mime-type. - /// - public static string GetMimeType(string path) - { - using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false)) - { - ISupportedImageFormat format = FormatUtilities.GetFormat(file); - - if (format != null) - { - return format.MimeType; - } - } - - return string.Empty; - } - /// /// Builds a regular expression from the type, this allows extensibility. /// diff --git a/src/ImageProcessor.Web/Helpers/RemoteFile.cs b/src/ImageProcessor.Web/Helpers/RemoteFile.cs index caff6afe3..cf2950c1d 100644 --- a/src/ImageProcessor.Web/Helpers/RemoteFile.cs +++ b/src/ImageProcessor.Web/Helpers/RemoteFile.cs @@ -173,16 +173,20 @@ namespace ImageProcessor.Web.Helpers /// internal async Task GetWebResponseAsync() { - WebResponse response; + WebResponse response = null; try { response = await this.GetWebRequest().GetResponseAsync(); } catch (WebException ex) { - if (ex.Status == WebExceptionStatus.NameResolutionFailure) + if (response != null) { - throw new HttpException(404, "No image exists at " + Uri); + HttpWebResponse errorResponse = (HttpWebResponse)ex.Response; + if (errorResponse.StatusCode == HttpStatusCode.NotFound) + { + throw new HttpException(404, "No image exists at " + this.Uri); + } } throw; @@ -217,7 +221,6 @@ namespace ImageProcessor.Web.Helpers #endregion #region Private - /// /// Creates the WebRequest object used internally for this RemoteFile instance. /// diff --git a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs index d78bbc485..ae07b8e63 100644 --- a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs +++ b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs @@ -148,7 +148,7 @@ namespace ImageProcessor.Web.HttpModules context.AddOnPostAuthorizeRequestAsync(postAuthorizeHelper.BeginEventHandler, postAuthorizeHelper.EndEventHandler); EventHandlerTaskAsyncHelper postProcessHelper = new EventHandlerTaskAsyncHelper(this.PostProcessImage); - context.AddOnPostRequestHandlerExecuteAsync(postProcessHelper.BeginEventHandler, postProcessHelper.EndEventHandler); + context.AddOnEndRequestAsync(postProcessHelper.BeginEventHandler, postProcessHelper.EndEventHandler); context.PreSendRequestHeaders += this.ContextPreSendRequestHeaders; } @@ -231,11 +231,11 @@ namespace ImageProcessor.Web.HttpModules if (cachedPathObject != null) { - string cachedPath = cachedPathObject.ToString(); - // Trim the cache. await this.imageCache.TrimCacheAsync(); + string cachedPath = cachedPathObject.ToString(); + // Fire the post processing event. EventHandler handler = OnPostProcessing; if (handler != null) @@ -300,6 +300,7 @@ namespace ImageProcessor.Web.HttpModules string queryString = string.Empty; string urlParameters = string.Empty; + // Legacy support. I'd like to remove this asap. if (hasMultiParams) { // We need to split the querystring to get the actual values we want. @@ -338,7 +339,13 @@ namespace ImageProcessor.Web.HttpModules } else { - requestPath = HttpUtility.UrlDecode(request.QueryString.ToString()); + // Parse any protocol values from settings. + string protocol = currentService.Settings["Protocol"] != null + ? currentService.Settings["Protocol"] + "://" + : string.Empty; + + requestPath = protocol + request.Path.Replace(currentService.Prefix, string.Empty).TrimStart('/'); + queryString = HttpUtility.UrlDecode(request.QueryString.ToString()); } } @@ -359,6 +366,7 @@ namespace ImageProcessor.Web.HttpModules string fullPath = string.Format("{0}{1}?{2}", requestPath, parts, queryString); object resourcePath; + // More legacy support code. if (hasMultiParams) { resourcePath = string.IsNullOrWhiteSpace(urlParameters) @@ -394,50 +402,55 @@ namespace ImageProcessor.Web.HttpModules { byte[] imageBuffer = await currentService.GetImage(resourcePath); - using (MemoryStream memoryStream = new MemoryStream(imageBuffer)) + using (MemoryStream inStream = 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(memoryStream); - memoryStream.Position = 0; + using (MemoryStream outStream = new MemoryStream()) + { + imageFactory.Load(inStream).AutoProcess(queryString).Save(outStream); - // Add to the cache. - await this.imageCache.AddImageToCacheAsync(memoryStream, imageFactory.CurrentImageFormat.MimeType); + // Add to the cache. + await this.imageCache.AddImageToCacheAsync(outStream, 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; - bool isFileCached = new Uri(cachedPath).IsFile; + // 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; + bool isFileCached = new Uri(cachedPath).IsFile; - if (isFileLocal) + if (isFileLocal) + { + if (isFileCached) { - if (isFileCached) - { - // 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] = Path.GetFileName(requestPath) == requestPath - ? null - : new List { requestPath }; - } + // 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 if (isFileCached) + else { - context.Items[CachedResponseFileDependency] = new List { cachedPath }; + context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath + ? null + : new List { requestPath }; } } + else if (isFileCached) + { + context.Items[CachedResponseFileDependency] = new List { cachedPath }; + } } } } // The cached file is valid so just rewrite the path. this.imageCache.RewritePath(context); + + // Redirect if not a locally store file. + if (!new Uri(cachedPath).IsFile) + { + context.ApplicationInstance.CompleteRequest(); + } } } @@ -557,11 +570,11 @@ namespace ImageProcessor.Web.HttpModules IImageService imageService = null; IList services = ImageProcessorConfiguration.Instance.ImageServices; - string path = request.Path; + string path = request.Path.TrimStart('/'); foreach (IImageService service in services) { string key = service.Prefix; - if (!string.IsNullOrWhiteSpace(key) && path.EndsWith(key, StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrWhiteSpace(key) && path.StartsWith(key, StringComparison.InvariantCultureIgnoreCase)) { imageService = service; } @@ -575,7 +588,6 @@ namespace ImageProcessor.Web.HttpModules // Return the file based service return services.FirstOrDefault(s => string.IsNullOrWhiteSpace(s.Prefix) && s.IsValidRequest(path)); } - #endregion } } \ No newline at end of file diff --git a/src/ImageProcessor.Web/Services/RemoteImageService.cs b/src/ImageProcessor.Web/Services/RemoteImageService.cs index a8a46319a..36888a6f0 100644 --- a/src/ImageProcessor.Web/Services/RemoteImageService.cs +++ b/src/ImageProcessor.Web/Services/RemoteImageService.cs @@ -37,7 +37,8 @@ namespace ImageProcessor.Web.Services this.Settings = new Dictionary { { "MaxBytes", "4194304" }, - { "Timeout", "30000" } + { "Timeout", "30000" }, + { "Protocol", "http" } }; this.WhiteList = new Uri[] { }; @@ -105,7 +106,7 @@ namespace ImageProcessor.Web.Services { if (!uri.IsAbsoluteUri) { - Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart(new[] { '.', '/' })); + Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart('.', '/')); validUrl = upper.StartsWith(rebaseUri.Host.ToUpperInvariant()) || upper.EndsWith(rebaseUri.Host.ToUpperInvariant()); } else diff --git a/src/ImageProcessor.sln.DotSettings b/src/ImageProcessor.sln.DotSettings index 0395ee1ba..d8fb7528a 100644 --- a/src/ImageProcessor.sln.DotSettings +++ b/src/ImageProcessor.sln.DotSettings @@ -18,5 +18,6 @@ REF RGB RGBA + SHA SRGB SS \ No newline at end of file diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index 0d07129e3..a473d8b63 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -139,6 +139,9 @@ namespace ImageProcessor /// public ImageFactory Load(Stream stream) { + // Reset the position of the stream to ensure we're reading the correct part. + stream.Position = 0; + ISupportedImageFormat format = FormatUtilities.GetFormat(stream); if (format == null) @@ -1070,8 +1073,9 @@ namespace ImageProcessor if (this.ShouldProcess) { // Allow the same stream to be used as for input. - stream.Position = 0; + stream.SetLength(0); this.Image = this.CurrentImageFormat.Save(stream, this.Image); + stream.Position = 0; } return this; diff --git a/src/ImageProcessor/Imaging/Formats/FormatBase.cs b/src/ImageProcessor/Imaging/Formats/FormatBase.cs index c74134670..cfad02046 100644 --- a/src/ImageProcessor/Imaging/Formats/FormatBase.cs +++ b/src/ImageProcessor/Imaging/Formats/FormatBase.cs @@ -108,7 +108,6 @@ namespace ImageProcessor.Imaging.Formats public virtual Image Save(Stream stream, Image image) { image.Save(stream, this.ImageFormat); - stream.Position = 0; return image; } diff --git a/src/TestWebsites/MVC/Global.asax.cs b/src/TestWebsites/MVC/Global.asax.cs index 11a3d306a..ef908f1ba 100644 --- a/src/TestWebsites/MVC/Global.asax.cs +++ b/src/TestWebsites/MVC/Global.asax.cs @@ -28,17 +28,17 @@ namespace Test_Website_NET45 RouteConfig.RegisterRoutes(RouteTable.Routes); // Test the post processing event. - ImageProcessingModule.OnPostProcessing += (sender, args) => Debug.WriteLine(args.CachedImagePath); + //ImageProcessingModule.OnPostProcessing += (sender, args) => Debug.WriteLine(args.CachedImagePath); - ImageProcessingModule.OnProcessQuerystring += (sender, args) => - { - if (!args.RawUrl.Contains("penguins")) - { - return args.Querystring += "watermark=protected&color=fff&fontsize=36&fontopacity=70textshadow=true&fontfamily=arial"; - } + //ImageProcessingModule.OnProcessQuerystring += (sender, args) => + // { + // if (!args.RawUrl.Contains("penguins")) + // { + // return args.Querystring += "watermark=protected&color=fff&fontsize=36&fontopacity=70textshadow=true&fontfamily=arial"; + // } - return args.Querystring; - }; + // return args.Querystring; + // }; } private async void WritePath(object sender, PostProcessingEventArgs e) diff --git a/src/TestWebsites/MVC/Views/Home/Index.cshtml b/src/TestWebsites/MVC/Views/Home/Index.cshtml index f010a23d9..4fed9c4b5 100644 --- a/src/TestWebsites/MVC/Views/Home/Index.cshtml +++ b/src/TestWebsites/MVC/Views/Home/Index.cshtml @@ -7,7 +7,8 @@

Resized

- + + @*

Foreign language test.

diff --git a/src/TestWebsites/MVC/config/imageprocessor/security.config b/src/TestWebsites/MVC/config/imageprocessor/security.config index dc9d8fe95..eb61ad133 100644 --- a/src/TestWebsites/MVC/config/imageprocessor/security.config +++ b/src/TestWebsites/MVC/config/imageprocessor/security.config @@ -6,8 +6,10 @@ + +