diff --git a/src/ImageProcessor.Web/NET4/ImageProcessor.Web.csproj b/src/ImageProcessor.Web/NET4/ImageProcessor.Web.csproj index 29e5f62f8..104e5c9f1 100644 --- a/src/ImageProcessor.Web/NET4/ImageProcessor.Web.csproj +++ b/src/ImageProcessor.Web/NET4/ImageProcessor.Web.csproj @@ -45,6 +45,7 @@ ..\..\packages\Csharp-Sqlite.3.7.7.1\lib\net40\Community.CsharpSqlite.SQLiteClient.dll + False ..\..\packages\Microsoft.Bcl.Async.1.0.16\lib\net40\Microsoft.Threading.Tasks.dll @@ -74,15 +75,12 @@ - - CacheManager.cs - - - CleanupImage.cs + + CacheIndexer.cs - - MemoryCache.cs + + MemCache.cs @@ -93,6 +91,9 @@ + + Preset.cs + diff --git a/src/ImageProcessor.Web/NET45/Caching/MemoryCache.cs b/src/ImageProcessor.Web/NET45/Caching/CacheIndexer.cs similarity index 82% rename from src/ImageProcessor.Web/NET45/Caching/MemoryCache.cs rename to src/ImageProcessor.Web/NET45/Caching/CacheIndexer.cs index dedaf5d56..0f3c678b7 100644 --- a/src/ImageProcessor.Web/NET45/Caching/MemoryCache.cs +++ b/src/ImageProcessor.Web/NET45/Caching/CacheIndexer.cs @@ -1,10 +1,10 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // -// +// Represents an in memory collection of keys and values whose operations are concurrent. // // -------------------------------------------------------------------------------------------------------------------- @@ -18,15 +18,15 @@ namespace ImageProcessor.Web.Caching /// /// Represents an in memory collection of keys and values whose operations are concurrent. /// - internal sealed class MemoryCache + internal sealed class CacheIndexer { #region Fields /// - /// A new instance Initializes a new instance of the class. + /// A new instance Initializes a new instance of the class. /// initialized lazily. /// - private static readonly Lazy Lazy = - new Lazy(() => new MemoryCache()); + private static readonly Lazy Lazy = + new Lazy(() => new CacheIndexer()); /// /// The object to lock against. @@ -36,19 +36,19 @@ namespace ImageProcessor.Web.Caching #region Constructors /// - /// Prevents a default instance of the class + /// Prevents a default instance of the class /// from being created. /// - private MemoryCache() + private CacheIndexer() { this.LoadCache(); } #endregion /// - /// Gets the current instance of the class. + /// Gets the current instance of the class. /// - public static MemoryCache Instance + public static CacheIndexer Instance { get { @@ -64,12 +64,12 @@ namespace ImageProcessor.Web.Caching /// The key of the value to get. /// /// - /// The matching the given key if the contains an element with + /// The matching the given key if the contains an element with /// the specified key; otherwise, null. /// public async Task GetValueAsync(string key) { - CachedImage cachedImage = (CachedImage)CacheManager.GetItem(key); + CachedImage cachedImage = (CachedImage)MemCache.GetItem(key); if (cachedImage == null) { @@ -77,7 +77,7 @@ namespace ImageProcessor.Web.Caching if (cachedImage != null) { - CacheManager.AddItem(key, cachedImage); + MemCache.AddItem(key, cachedImage); } } @@ -91,14 +91,14 @@ namespace ImageProcessor.Web.Caching /// The key of the item to remove. /// /// - /// true if the removes an element with + /// true if the removes an element with /// the specified key; otherwise, false. /// public async Task RemoveAsync(string key) { if (await this.SaveCacheAsync(key, null, true) > 0) { - CacheManager.RemoveItem(key); + MemCache.RemoveItem(key); return true; } @@ -122,7 +122,7 @@ namespace ImageProcessor.Web.Caching // Add the CachedImage. if (await this.SaveCacheAsync(key, cachedImage, false) > 0) { - CacheManager.AddItem(key, cachedImage); + MemCache.AddItem(key, cachedImage); } return cachedImage; @@ -130,7 +130,7 @@ namespace ImageProcessor.Web.Caching #endregion /// - /// Saves the in memory cache to the file-system. + /// Saves the image to the file-system cache. /// /// /// The key. diff --git a/src/ImageProcessor.Web/NET45/Caching/CleanupImage.cs b/src/ImageProcessor.Web/NET45/Caching/CleanupImage.cs deleted file mode 100644 index aa813bae3..000000000 --- a/src/ImageProcessor.Web/NET45/Caching/CleanupImage.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. -// -// -// Describes a cached image for cleanup. -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace ImageProcessor.Web.Caching -{ - #region Using - using System; - #endregion - - /// - /// Describes a cached image for cleanup - /// - public sealed class CleanupImage - { - /// - /// Gets or sets the value of the cached image. - /// - public string Path { get; set; } - - /// - /// Gets or sets when the cached image should expire from the cache. - /// - public DateTime ExpiresUtc { get; set; } - } -} diff --git a/src/ImageProcessor.Web/NET45/Caching/DiskCache.cs b/src/ImageProcessor.Web/NET45/Caching/DiskCache.cs index d8572050d..36e3afa7c 100644 --- a/src/ImageProcessor.Web/NET45/Caching/DiskCache.cs +++ b/src/ImageProcessor.Web/NET45/Caching/DiskCache.cs @@ -12,11 +12,11 @@ namespace ImageProcessor.Web.Caching { #region Using using System; + using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; - using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; @@ -47,17 +47,7 @@ namespace ImageProcessor.Web.Caching /// /// /// - private const int MaxFilesCount = 100; - - /// - /// The regular expression to search strings for valid subfolder names. - /// We're specifically not using a shorter regex as we need to be able to iterate through - /// each match group. - /// - private static readonly Regex SubFolderRegex = - new Regex( - @"(\/([a-z]|[0-9])\/(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|0|1|2|3|4|5|6|7|8|9)\/)", - RegexOptions.Compiled); + private const int MaxFilesCount = 50; /// /// The absolute path to virtual cache path on the server. @@ -171,7 +161,7 @@ namespace ImageProcessor.Web.Caching ExpiresUtc = expires }; - await MemoryCache.Instance.AddAsync(key, cachedImage); + await CacheIndexer.Instance.AddAsync(key, cachedImage); } /// @@ -188,7 +178,7 @@ namespace ImageProcessor.Web.Caching if (this.isRemote) { - cachedImage = await MemoryCache.Instance.GetValueAsync(key); + cachedImage = await CacheIndexer.Instance.GetValueAsync(key); if (cachedImage != null) { @@ -197,7 +187,7 @@ namespace ImageProcessor.Web.Caching if (cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration) || cachedImage.MaxAge != MaxFileCachedDuration) { - if (await MemoryCache.Instance.RemoveAsync(key)) + if (await CacheIndexer.Instance.RemoveAsync(key)) { isUpdated = true; } @@ -212,7 +202,7 @@ namespace ImageProcessor.Web.Caching else { // Test now for locally requested files. - cachedImage = await MemoryCache.Instance.GetValueAsync(key); + cachedImage = await CacheIndexer.Instance.GetValueAsync(key); if (cachedImage != null) { @@ -226,7 +216,7 @@ namespace ImageProcessor.Web.Caching || cachedImage.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration) || cachedImage.MaxAge != MaxFileCachedDuration) { - if (await MemoryCache.Instance.RemoveAsync(key)) + if (await CacheIndexer.Instance.RemoveAsync(key)) { isUpdated = true; } @@ -254,7 +244,7 @@ namespace ImageProcessor.Web.Caching string key = Path.GetFileNameWithoutExtension(this.CachedPath); DateTime dateTime = DateTime.UtcNow; - CachedImage cachedImage = await MemoryCache.Instance.GetValueAsync(key); + CachedImage cachedImage = await CacheIndexer.Instance.GetValueAsync(key); if (cachedImage != null) { @@ -277,15 +267,17 @@ namespace ImageProcessor.Web.Caching } /// - /// Purges any files from the file-system cache in the given folders. + /// Trims a cached folder ensuring that it does not exceed the maximum file count. /// + /// + /// The path to the folder. + /// /// /// The . /// - internal async Task TrimCachedFoldersAsync() + internal async Task TrimCachedFolderAsync(string path) { - // Create Action delegate for TrimCachedFolders. - await TaskHelpers.Run(this.TrimCachedFolders); + await TaskHelpers.Run(() => this.TrimCachedFolder(path)); } #endregion @@ -325,50 +317,42 @@ namespace ImageProcessor.Web.Caching } /// - /// Purges any files from the file-system cache in the given folders. + /// Trims a cached folder ensuring that it does not exceed the maximum file count. /// - private async void TrimCachedFolders() + /// + /// The path to the folder. + /// + private async void TrimCachedFolder(string path) { - // Group each cache folder and clear any expired items or any that exceed - // the maximum allowable count. - var groups = SQLContext.GetImagesForCleanup() - .GroupBy(x => SubFolderRegex.Match(x.Path).Value) - .Where(g => g.Count() > MaxFilesCount); + // ReSharper disable once AssignNullToNotNullAttribute + DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(path)); + IEnumerable files = directoryInfo.EnumerateFiles().OrderBy(f => f.LastWriteTimeUtc); + int count = files.Count(); - foreach (var group in groups) + foreach (FileInfo fileInfo in files) { - int groupCount = group.Count(); - - foreach (CleanupImage image in group.OrderBy(x => x.ExpiresUtc)) + try { // If the group count is equal to the max count minus 1 then we know we - // are counting down from a full directory not simply clearing out - // expired items. - if (groupCount <= MaxFilesCount - 1 - && image.ExpiresUtc >= DateTime.UtcNow.AddDays(-MaxFileCachedDuration)) + // have reduced the number of items below the maximum allowed. + if (count <= MaxFilesCount - 1) { break; } - try + // Remove from the cache and delete each CachedImage. + string key = Path.GetFileNameWithoutExtension(fileInfo.Name); + if (await CacheIndexer.Instance.RemoveAsync(key)) { - // Remove from the cache and delete each CachedImage. - FileInfo fileInfo = new FileInfo(image.Path); - string key = Path.GetFileNameWithoutExtension(fileInfo.Name); - - if (await MemoryCache.Instance.RemoveAsync(key)) - { - fileInfo.Delete(); - groupCount -= 1; - } - } - // ReSharper disable EmptyGeneralCatchClause - catch - // ReSharper restore EmptyGeneralCatchClause - { - // Do nothing; skip to the next file. + fileInfo.Delete(); + count -= 1; } } + // ReSharper disable once EmptyGeneralCatchClause + catch + { + // Do nothing; skip to the next file. + } } } @@ -376,8 +360,8 @@ namespace ImageProcessor.Web.Caching /// Gets the full transformed cached path 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 40 folders within 40 folders giving us a total of 3.0223145e+64 potential images. - /// Answers on a post card if you can figure out a way to store their details in a db for fast recovery. + /// This allows us to store millions of images. + /// Answers on a post card if you can figure out a way to store that many details in a db for fast recovery. /// /// The full cached path for the image. [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] @@ -394,7 +378,8 @@ namespace ImageProcessor.Web.Caching string fallbackExtension = this.imageName.Substring(this.imageName.LastIndexOf(".", StringComparison.Ordinal) + 1); string encryptedName = this.fullPath.ToSHA1Fingerprint(); - string pathFromKey = string.Join("\\", encryptedName.ToCharArray()); + // Collision rate of about 1 in 1000 for the folder structure. + string pathFromKey = string.Join("\\", encryptedName.ToCharArray().Take(5)); string cachedFileName = string.Format( "{0}.{1}", diff --git a/src/ImageProcessor.Web/NET45/Caching/CacheManager.cs b/src/ImageProcessor.Web/NET45/Caching/MemCache.cs similarity index 97% rename from src/ImageProcessor.Web/NET45/Caching/CacheManager.cs rename to src/ImageProcessor.Web/NET45/Caching/MemCache.cs index 0e1e517b5..bd7c471d0 100644 --- a/src/ImageProcessor.Web/NET45/Caching/CacheManager.cs +++ b/src/ImageProcessor.Web/NET45/Caching/MemCache.cs @@ -1,10 +1,10 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // -// Encapsulates methods that allow the caching and retrieval of objects. +// Encapsulates methods that allow the caching and retrieval of objects from the in memory cache. // // -------------------------------------------------------------------------------------------------------------------- @@ -17,15 +17,15 @@ namespace ImageProcessor.Web.Caching #endregion /// - /// Encapsulates methods that allow the caching and retrieval of objects. + /// Encapsulates methods that allow the caching and retrieval of objects from the in memory cache. /// - public static class CacheManager + internal static class MemCache { #region Fields /// /// The cache /// - private static readonly ObjectCache Cache = System.Runtime.Caching.MemoryCache.Default; + private static readonly ObjectCache Cache = MemoryCache.Default; /// /// An internal list of cache keys to allow bulk removal. @@ -99,8 +99,6 @@ namespace ImageProcessor.Web.Caching return Cache.Get(key, regionName); } - //public static bool - /// /// Updates an item to the cache. /// diff --git a/src/ImageProcessor.Web/NET45/Caching/SQLContext.cs b/src/ImageProcessor.Web/NET45/Caching/SQLContext.cs index deeb30658..464bf56e0 100644 --- a/src/ImageProcessor.Web/NET45/Caching/SQLContext.cs +++ b/src/ImageProcessor.Web/NET45/Caching/SQLContext.cs @@ -1,24 +1,21 @@ -// ----------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. // -// ----------------------------------------------------------------------- +// +// Provides a wrapper for the SQLite functionality. +// +// -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Web.Caching { #region Using - - using System; - using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using System.Web.Hosting; using ImageProcessor.Web.Config; - using ImageProcessor.Web.Helpers; - using SQLite; - #endregion /// @@ -79,31 +76,6 @@ namespace ImageProcessor.Web.Caching } } - /// - /// Gets all the images from the database. - /// - /// - /// The . - /// - internal static List GetImagesForCleanup() - { - try - { - List images; - using (SQLiteConnection connection = new SQLiteConnection(ConnectionString)) - { - images = connection.Query("SELECT Path,ExpiresUtc FROM CachedImage"); - } - - return images; - - } - catch - { - return new List(); - } - } - /// /// Gets a cached image from the database. /// diff --git a/src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs index 73f7b813d..8dd9defd6 100644 --- a/src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs +++ b/src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs @@ -20,12 +20,10 @@ namespace ImageProcessor.Web.HttpModules using System.Security; using System.Security.Permissions; using System.Security.Principal; - using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using System.Web.Security; - using ImageProcessor.Helpers.Extensions; using ImageProcessor.Imaging; using ImageProcessor.Web.Caching; @@ -53,16 +51,6 @@ namespace ImageProcessor.Web.HttpModules /// The assembly version. /// private static readonly string AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - - /// - /// The value that acts as a basis to check that the startup code has only been ran once. - /// - private static int initCheck; - - /// - /// A value indicating whether the application has started. - /// - private readonly bool hasModuleInitialized = initCheck == 1; #endregion #region IHttpModule Members @@ -76,12 +64,6 @@ namespace ImageProcessor.Web.HttpModules /// public void Init(HttpApplication context) { - if (!this.hasModuleInitialized) - { - Interlocked.CompareExchange(ref initCheck, 1, 0); - // DiskCache.CreateDirectories(); - } - #if NET45 EventHandlerTaskAsyncHelper wrapper = new EventHandlerTaskAsyncHelper(this.PostAuthorizeRequest); @@ -288,6 +270,8 @@ namespace ImageProcessor.Web.HttpModules // Only process if the file has been updated. if (isNewOrUpdated) { + string cachedPath = cache.CachedPath; + // Process the image. using (ImageFactory imageFactory = new ImageFactory()) { @@ -308,21 +292,21 @@ namespace ImageProcessor.Web.HttpModules { if (responseStream != null) { - // Trim the cache. - await cache.TrimCachedFoldersAsync(); - responseStream.CopyTo(memoryStream); imageFactory.Load(memoryStream) .AddQueryString(queryString) .Format(ImageUtils.GetImageFormat(imageName)) - .AutoProcess().Save(cache.CachedPath); + .AutoProcess().Save(cachedPath); // Ensure that the LastWriteTime property of the source and cached file match. DateTime dateTime = await cache.SetCachedLastWriteTimeAsync(); // Add to the cache. await cache.AddImageToCacheAsync(dateTime); + + // Trim the cache. + await cache.TrimCachedFolderAsync(cachedPath); } } } @@ -330,16 +314,16 @@ namespace ImageProcessor.Web.HttpModules } else { - // Trim the cache. - await cache.TrimCachedFoldersAsync(); - - imageFactory.Load(fullPath).AutoProcess().Save(cache.CachedPath); + imageFactory.Load(fullPath).AutoProcess().Save(cachedPath); // Ensure that the LastWriteTime property of the source and cached file match. DateTime dateTime = await cache.SetCachedLastWriteTimeAsync(); // Add to the cache. await cache.AddImageToCacheAsync(dateTime); + + // Trim the cache. + await cache.TrimCachedFolderAsync(cachedPath); } } } diff --git a/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj b/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj index 59cd99bfc..fcd3a82a9 100644 --- a/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj +++ b/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj @@ -50,10 +50,9 @@ - - + - + diff --git a/src/ImageProcessor.Web/NET45/Properties/AssemblyInfo.cs b/src/ImageProcessor.Web/NET45/Properties/AssemblyInfo.cs index d2b87b19c..b97332e79 100644 --- a/src/ImageProcessor.Web/NET45/Properties/AssemblyInfo.cs +++ b/src/ImageProcessor.Web/NET45/Properties/AssemblyInfo.cs @@ -31,5 +31,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("2.3.0.6")] -[assembly: AssemblyFileVersion("2.3.0.6")] +[assembly: AssemblyVersion("2.4.0.0")] +[assembly: AssemblyFileVersion("2.4.0.0")] diff --git a/src/ImageProcessor/Properties/AssemblyInfo.cs b/src/ImageProcessor/Properties/AssemblyInfo.cs index c5c28955b..6541db302 100644 --- a/src/ImageProcessor/Properties/AssemblyInfo.cs +++ b/src/ImageProcessor/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ using System.Security; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("1.7.1.1")] -[assembly: AssemblyFileVersion("1.7.1.1")] +[assembly: AssemblyVersion("1.8.0.0")] +[assembly: AssemblyFileVersion("1.8.0.0")] diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj b/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj index 318733563..562e69eb5 100644 --- a/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj +++ b/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj @@ -193,7 +193,9 @@ - + + Designer + diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml index 52734d05a..55cc2db9d 100644 --- a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml +++ b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml @@ -141,7 +141,7 @@

Color Profiles

-
+@*

CMYK original jpg

@@ -151,9 +151,8 @@

sRGB original jpg

-
-
+
*@
diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Png.cshtml b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Png.cshtml index 3b4ff8b45..d01e7ae8c 100644 --- a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Png.cshtml +++ b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Png.cshtml @@ -8,7 +8,6 @@

Resized

-

Cropped