Skip to content

Commit b15c4d4

Browse files
Merge pull request #213 from ronaldbarendse/feature/cache-folderdepth
Add CacheFolderDepth option and create shorter file names
2 parents fe0b4e9 + 5eac249 commit b15c4d4

12 files changed

Lines changed: 82 additions & 72 deletions

File tree

samples/ImageSharp.Web.Sample/Startup.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,10 @@ public void ConfigureServices(IServiceCollection services)
5151
return new PhysicalFileSystemCache(
5252
provider.GetRequiredService<IOptions<PhysicalFileSystemCacheOptions>>(),
5353
provider.GetRequiredService<IWebHostEnvironment>(),
54-
provider.GetRequiredService<IOptions<ImageSharpMiddlewareOptions>>(),
5554
provider.GetRequiredService<FormatUtilities>());
5655
})
5756
.SetCacheKey<UriRelativeLowerInvariantCacheKey>()
58-
.SetCacheHash<CacheHash>()
57+
.SetCacheHash<SHA256CacheHash>()
5958
.AddProvider<PhysicalFileSystemProvider>()
6059
.AddProcessor<ResizeWebProcessor>()
6160
.AddProcessor<FormatWebProcessor>()
@@ -88,7 +87,7 @@ private void ConfigureDefaultServicesAndCustomOptions(IServiceCollection service
8887
options.Configuration = Configuration.Default;
8988
options.BrowserMaxAge = TimeSpan.FromDays(7);
9089
options.CacheMaxAge = TimeSpan.FromDays(365);
91-
options.CachedNameLength = 8;
90+
options.CacheHashLength = 8;
9291
options.OnParseCommandsAsync = _ => Task.CompletedTask;
9392
options.OnBeforeSaveAsync = _ => Task.CompletedTask;
9493
options.OnProcessedAsync = _ => Task.CompletedTask;
@@ -111,7 +110,7 @@ private void ConfigureCustomServicesAndCustomOptions(IServiceCollection services
111110
options.Configuration = Configuration.Default;
112111
options.BrowserMaxAge = TimeSpan.FromDays(7);
113112
options.CacheMaxAge = TimeSpan.FromDays(365);
114-
options.CachedNameLength = 8;
113+
options.CacheHashLength = 8;
115114
options.OnParseCommandsAsync = _ => Task.CompletedTask;
116115
options.OnBeforeSaveAsync = _ => Task.CompletedTask;
117116
options.OnProcessedAsync = _ => Task.CompletedTask;
@@ -127,11 +126,10 @@ private void ConfigureCustomServicesAndCustomOptions(IServiceCollection services
127126
return new PhysicalFileSystemCache(
128127
provider.GetRequiredService<IOptions<PhysicalFileSystemCacheOptions>>(),
129128
provider.GetRequiredService<IWebHostEnvironment>(),
130-
provider.GetRequiredService<IOptions<ImageSharpMiddlewareOptions>>(),
131129
provider.GetRequiredService<FormatUtilities>());
132130
})
133131
.SetCacheKey<UriRelativeLowerInvariantCacheKey>()
134-
.SetCacheHash<CacheHash>()
132+
.SetCacheHash<SHA256CacheHash>()
135133
.ClearProviders()
136134
.AddProvider<PhysicalFileSystemProvider>()
137135
.ClearProcessors()

src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Microsoft.AspNetCore.Hosting;
1010
using Microsoft.Extensions.FileProviders;
1111
using Microsoft.Extensions.Options;
12-
using SixLabors.ImageSharp.Web.Middleware;
1312
using SixLabors.ImageSharp.Web.Resolvers;
1413

1514
namespace SixLabors.ImageSharp.Web.Caching
@@ -25,25 +24,15 @@ public class PhysicalFileSystemCache : IImageCache
2524
private readonly string cacheRootPath;
2625

2726
/// <summary>
28-
/// The length of the filename to use (minus the extension) when storing images in the image cache.
27+
/// The depth of the nested cache folders structure to store the images.
2928
/// </summary>
30-
private readonly int cachedNameLength;
29+
private readonly int cacheFolderDepth;
3130

3231
/// <summary>
3332
/// The file provider abstraction.
3433
/// </summary>
3534
private readonly IFileProvider fileProvider;
3635

37-
/// <summary>
38-
/// The cache configuration options.
39-
/// </summary>
40-
private readonly PhysicalFileSystemCacheOptions cacheOptions;
41-
42-
/// <summary>
43-
/// The middleware configuration options.
44-
/// </summary>
45-
private readonly ImageSharpMiddlewareOptions options;
46-
4736
/// <summary>
4837
/// Contains various format helper methods based on the current configuration.
4938
/// </summary>
@@ -52,35 +41,31 @@ public class PhysicalFileSystemCache : IImageCache
5241
/// <summary>
5342
/// Initializes a new instance of the <see cref="PhysicalFileSystemCache"/> class.
5443
/// </summary>
55-
/// <param name="cacheOptions">The cache configuration options.</param>
44+
/// <param name="options">The cache configuration options.</param>
5645
/// <param name="environment">The hosting environment the application is running in.</param>
57-
/// <param name="options">The middleware configuration options.</param>
5846
/// <param name="formatUtilities">Contains various format helper methods based on the current configuration.</param>
5947
public PhysicalFileSystemCache(
60-
IOptions<PhysicalFileSystemCacheOptions> cacheOptions,
48+
IOptions<PhysicalFileSystemCacheOptions> options,
6149
#if NETCOREAPP2_1
6250
IHostingEnvironment environment,
6351
#else
6452
IWebHostEnvironment environment,
6553
#endif
66-
IOptions<ImageSharpMiddlewareOptions> options,
6754
FormatUtilities formatUtilities)
6855
{
6956
Guard.NotNull(environment, nameof(environment));
7057
Guard.NotNull(options, nameof(options));
7158
Guard.NotNullOrWhiteSpace(environment.WebRootPath, nameof(environment.WebRootPath));
7259

73-
// Allow configuration of the cache without having to register everything.
74-
this.cacheOptions = cacheOptions != null ? cacheOptions.Value : new PhysicalFileSystemCacheOptions();
75-
this.cacheRootPath = GetCacheRoot(this.cacheOptions, environment.WebRootPath, environment.ContentRootPath);
76-
if (!Directory.Exists(this.cacheRootPath))
77-
{
78-
Directory.CreateDirectory(this.cacheRootPath);
79-
}
60+
// Allow configuration of the cache without having to register everything
61+
PhysicalFileSystemCacheOptions cacheOptions = options != null ? options.Value : new PhysicalFileSystemCacheOptions();
62+
this.cacheRootPath = GetCacheRoot(cacheOptions, environment.WebRootPath, environment.ContentRootPath);
63+
this.cacheFolderDepth = (int)cacheOptions.CacheFolderDepth;
64+
65+
// Ensure cache directory is created before initializing the file provider
66+
Directory.CreateDirectory(this.cacheRootPath);
8067

8168
this.fileProvider = new PhysicalFileProvider(this.cacheRootPath);
82-
this.options = options.Value;
83-
this.cachedNameLength = (int)this.options.CachedNameLength;
8469
this.formatUtilities = formatUtilities;
8570
}
8671

@@ -105,7 +90,7 @@ internal static string GetCacheRoot(PhysicalFileSystemCacheOptions cacheOptions,
10590
/// <inheritdoc/>
10691
public Task<IImageCacheResolver> GetAsync(string key)
10792
{
108-
string path = ToFilePath(key, this.cachedNameLength);
93+
string path = ToFilePath(key, this.cacheFolderDepth);
10994

11095
IFileInfo metaFileInfo = this.fileProvider.GetFileInfo(this.ToMetaDataFilePath(path));
11196
if (!metaFileInfo.Exists)
@@ -119,15 +104,13 @@ public Task<IImageCacheResolver> GetAsync(string key)
119104
/// <inheritdoc/>
120105
public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
121106
{
122-
string path = Path.Combine(this.cacheRootPath, ToFilePath(key, this.cachedNameLength));
107+
string path = Path.Combine(this.cacheRootPath, ToFilePath(key, this.cacheFolderDepth));
123108
string imagePath = this.ToImageFilePath(path, metadata);
124109
string metaPath = this.ToMetaDataFilePath(path);
125110
string directory = Path.GetDirectoryName(path);
126111

127-
if (!Directory.Exists(directory))
128-
{
129-
Directory.CreateDirectory(directory);
130-
}
112+
// Ensure cache directory is created before creating files
113+
Directory.CreateDirectory(directory);
131114

132115
using (FileStream fileStream = File.Create(imagePath))
133116
{
@@ -162,13 +145,35 @@ private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
162145
/// Converts the key into a nested file path.
163146
/// </summary>
164147
/// <param name="key">The cache key.</param>
165-
/// <param name="cachedNameLength">The length of the cached file name minus the extension.</param>
166-
/// <returns>The <see cref="string"/>.</returns>
148+
/// <param name="cacheFolderDepth">The depth of the nested cache folders structure to store the images.</param>
149+
/// <returns>
150+
/// The <see cref="string" />.
151+
/// </returns>
167152
[MethodImpl(MethodImplOptions.AggressiveInlining)]
168-
internal static unsafe string ToFilePath(string key, int cachedNameLength)
153+
internal static unsafe string ToFilePath(string key, int cacheFolderDepth)
169154
{
170-
// Each key substring char + separator + key
171-
int length = (cachedNameLength * 2) + key.Length;
155+
if (cacheFolderDepth == 0)
156+
{
157+
// Short-circuit when not nesting folders
158+
return key;
159+
}
160+
161+
int length;
162+
int nameStartIndex;
163+
if (cacheFolderDepth >= key.Length)
164+
{
165+
// Keep all characters in file name (legacy behavior)
166+
cacheFolderDepth = key.Length;
167+
length = (cacheFolderDepth * 2) + key.Length;
168+
nameStartIndex = 0;
169+
}
170+
else
171+
{
172+
// Remove characters used in folders from file name
173+
length = cacheFolderDepth + key.Length;
174+
nameStartIndex = cacheFolderDepth;
175+
}
176+
172177
fixed (char* keyPtr = key)
173178
{
174179
return string.Create(length, (Ptr: (IntPtr)keyPtr, key.Length), (chars, args) =>
@@ -179,13 +184,13 @@ internal static unsafe string ToFilePath(string key, int cachedNameLength)
179184
ref char charRef = ref MemoryMarshal.GetReference(chars);
180185

181186
int index = 0;
182-
for (int i = 0; i < cachedNameLength; i++)
187+
for (int i = 0; i < cacheFolderDepth; i++)
183188
{
184189
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
185190
Unsafe.Add(ref charRef, index++) = separator;
186191
}
187192

188-
for (int i = 0; i < keySpan.Length; i++)
193+
for (int i = nameStartIndex; i < keySpan.Length; i++)
189194
{
190195
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
191196
}

src/ImageSharp.Web/Caching/PhysicalFileSystemCacheOptions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace SixLabors.ImageSharp.Web.Caching
55
{
66
/// <summary>
7-
/// Configuration options for the <see cref="PhysicalFileSystemCache"/>.
7+
/// Configuration options for the <see cref="PhysicalFileSystemCache" />.
88
/// </summary>
99
public class PhysicalFileSystemCacheOptions
1010
{
@@ -13,6 +13,11 @@ public class PhysicalFileSystemCacheOptions
1313
/// </summary>
1414
public string CacheFolder { get; set; } = "is-cache";
1515

16+
/// <summary>
17+
/// Gets or sets the depth of the nested cache folders structure to store the images. Defaults to 8.
18+
/// </summary>
19+
public uint CacheFolderDepth { get; set; } = 8;
20+
1621
/// <summary>
1722
/// Gets or sets the optional cache root folder.
1823
/// <para>

src/ImageSharp.Web/Caching/CacheHash.cs renamed to src/ImageSharp.Web/Caching/SHA256CacheHash.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ namespace SixLabors.ImageSharp.Web.Caching
1616
/// Hashed keys are the result of the SHA256 computation of the input value for the given length.
1717
/// This ensures low collision rates with a shorter file name.
1818
/// </summary>
19-
public sealed class CacheHash : ICacheHash
19+
public sealed class SHA256CacheHash : ICacheHash
2020
{
2121
/// <summary>
22-
/// Initializes a new instance of the <see cref="CacheHash"/> class.
22+
/// Initializes a new instance of the <see cref="SHA256CacheHash"/> class.
2323
/// </summary>
2424
/// <param name="options">The middleware configuration options.</param>
25-
public CacheHash(IOptions<ImageSharpMiddlewareOptions> options)
25+
public SHA256CacheHash(IOptions<ImageSharpMiddlewareOptions> options)
2626
{
2727
Guard.NotNull(options, nameof(options));
28-
Guard.MustBeBetweenOrEqualTo<uint>(options.Value.CachedNameLength, 2, 64, nameof(options.Value.CachedNameLength));
28+
Guard.MustBeBetweenOrEqualTo<uint>(options.Value.CacheHashLength, 2, 64, nameof(options.Value.CacheHashLength));
2929
}
3030

3131
/// <inheritdoc/>
@@ -34,8 +34,7 @@ public string Create(string value, uint length)
3434
{
3535
int byteCount = Encoding.ASCII.GetByteCount(value);
3636

37-
// Allocating a buffer from the pool is ~27% slower than stackalloc so use
38-
// that for short strings
37+
// Allocating a buffer from the pool is ~27% slower than stackalloc so use that for short strings
3938
if (byteCount < 257)
4039
{
4140
return HashValue(value, length, stackalloc byte[byteCount]);
@@ -62,11 +61,11 @@ private static string HashValue(ReadOnlySpan<char> value, uint length, Span<byte
6261
using var hashAlgorithm = SHA256.Create();
6362
Encoding.ASCII.GetBytes(value, bufferSpan);
6463

65-
// Hashed output maxes out at 32 bytes @ 256bit/8 so we're safe to use stackalloc.
64+
// Hashed output maxes out at 32 bytes @ 256bit/8 so we're safe to use stackalloc
6665
Span<byte> hash = stackalloc byte[32];
6766
hashAlgorithm.TryComputeHash(bufferSpan, hash, out int _);
6867

69-
// length maxes out at 64 since we throw if options is greater.
68+
// Length maxes out at 64 since we throw if options is greater
7069
return HexEncoder.Encode(hash.Slice(0, (int)(length / 2)));
7170
}
7271
}

src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private static void AddDefaultServices(
6666

6767
builder.SetCacheKey<UriRelativeLowerInvariantCacheKey>();
6868

69-
builder.SetCacheHash<CacheHash>();
69+
builder.SetCacheHash<SHA256CacheHash>();
7070

7171
builder.AddProvider<PhysicalFileSystemProvider>();
7272

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ private async Task ProcessRequestAsync(
271271
// Create a hashed cache key
272272
string key = this.cacheHash.Create(
273273
this.cacheKey.Create(context, commands),
274-
this.options.CachedNameLength);
274+
this.options.CacheHashLength);
275275

276276
// Check the cache, if present, not out of date and not requiring an update
277277
// we'll simply serve the file from there.

src/ImageSharp.Web/Middleware/ImageSharpMiddlewareOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public class ImageSharpMiddlewareOptions
8888
/// Gets or sets the length of the filename to use (minus the extension) when storing
8989
/// images in the image cache. Defaults to 12 characters.
9090
/// </summary>
91-
public uint CachedNameLength { get; set; } = 12;
91+
public uint CacheHashLength { get; set; } = 12;
9292

9393
/// <summary>
9494
/// Gets or sets the additional command parsing method that can be used to used to augment commands.

tests/ImageSharp.Web.Benchmarks/Caching/CacheHashBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class CacheHashBenchmarks
1313
{
1414
private const string URL = "http://testwebsite.com/image-12345.jpeg?width=400";
1515
private static readonly IOptions<ImageSharpMiddlewareOptions> MWOptions = Options.Create(new ImageSharpMiddlewareOptions());
16-
private static readonly CacheHash Sha256Hasher = new CacheHash(MWOptions);
16+
private static readonly SHA256CacheHash Sha256Hasher = new SHA256CacheHash(MWOptions);
1717
private static readonly CacheHashBaseline NaiveSha256Hasher = new CacheHashBaseline();
1818

1919
[Benchmark(Baseline = true, Description = "Baseline Sha256Hasher")]

tests/ImageSharp.Web.Benchmarks/Caching/StringJoinBenchmarks.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ namespace SixLabors.ImageSharp.Web.Benchmarks.Caching
1212
public class StringJoinBenchmarks
1313
{
1414
private const string Key = "abcdefghijkl";
15-
private const int CachedNameLength = 12;
15+
16+
[Params(0, 8, 12)]
17+
public int CacheFolderDepth { get; set; }
1618

1719
[Benchmark(Baseline = true, Description = "String.Join")]
1820
public string JoinUsingString()
19-
=> $"{string.Join("/", Key.Substring(0, CachedNameLength).ToCharArray())}/{Key}";
21+
=> $"{string.Join("/", Key.Substring(0, this.CacheFolderDepth).ToCharArray())}/{Key}";
2022

2123
[Benchmark(Description = "StringBuilder.Append")]
2224
public string JoinUsingStringBuilder()
@@ -25,8 +27,8 @@ public string JoinUsingStringBuilder()
2527
const char separator = '/';
2628

2729
// Each key substring char + separator + key
28-
var sb = new StringBuilder((CachedNameLength * 2) + Key.Length);
29-
ReadOnlySpan<char> paths = keySpan.Slice(0, CachedNameLength);
30+
var sb = new StringBuilder((this.CacheFolderDepth * 2) + Key.Length);
31+
ReadOnlySpan<char> paths = keySpan.Slice(0, this.CacheFolderDepth);
3032
for (int i = 0; i < paths.Length; i++)
3133
{
3234
sb.Append(paths[i]);
@@ -39,7 +41,7 @@ public string JoinUsingStringBuilder()
3941

4042
[Benchmark(Description = "String.Create")]
4143
public string JoinUsingStringCreate()
42-
=> PhysicalFileSystemCache.ToFilePath(Key, CachedNameLength);
44+
=> PhysicalFileSystemCache.ToFilePath(Key, this.CacheFolderDepth);
4345

4446
/*
4547
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363

tests/ImageSharp.Web.Tests/Caching/CacheHashTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Web.Tests.Caching
1313
public class CacheHashTests
1414
{
1515
private static readonly IOptions<ImageSharpMiddlewareOptions> Options = MSOptions.Create(new ImageSharpMiddlewareOptions());
16-
private static readonly ICacheHash CacheHash = new CacheHash(Options);
16+
private static readonly ICacheHash CacheHash = new SHA256CacheHash(Options);
1717

1818
[Fact]
1919
public void CacheHashProducesIdenticalResults()

0 commit comments

Comments
 (0)