mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
250 lines
9.5 KiB
250 lines
9.5 KiB
// --------------------------------------------------------------------------------------------------------------------
|
|
// <copyright file="DiskCache.cs" company="James South">
|
|
// Copyright (c) James South.
|
|
// Licensed under the Apache License, Version 2.0.
|
|
// </copyright>
|
|
// <summary>
|
|
// Provides an <see cref="IImageCache" /> implementation that is file system based.
|
|
// The cache is self healing and cleaning.
|
|
// </summary>
|
|
// --------------------------------------------------------------------------------------------------------------------
|
|
|
|
namespace ImageProcessor.Web.Caching
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Configuration;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
using System.Web.Hosting;
|
|
|
|
using ImageProcessor.Web.Extensions;
|
|
|
|
/// <summary>
|
|
/// Provides an <see cref="IImageCache"/> implementation that is file system based.
|
|
/// The cache is self healing and cleaning.
|
|
/// </summary>
|
|
public class DiskCache : ImageCacheBase
|
|
{
|
|
/// <summary>
|
|
/// The maximum number of files allowed in the directory.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// <see href="http://stackoverflow.com/questions/197162/ntfs-performance-and-large-volumes-of-files-and-directories"/>
|
|
/// <see href="http://stackoverflow.com/questions/115882/how-do-you-deal-with-lots-of-small-files"/>
|
|
/// <see href="http://stackoverflow.com/questions/1638219/millions-of-small-graphics-files-and-how-to-overcome-slow-file-system-access-on"/>
|
|
/// </remarks>
|
|
private const int MaxFilesCount = 100;
|
|
|
|
/// <summary>
|
|
/// The maximum number of days to store the image.
|
|
/// </summary>
|
|
private readonly int maxDays;
|
|
|
|
/// <summary>
|
|
/// The virtual cache path.
|
|
/// </summary>
|
|
private readonly string virtualCachePath;
|
|
|
|
/// <summary>
|
|
/// The absolute path to virtual cache path on the server.
|
|
/// </summary>
|
|
private readonly string absoluteCachePath;
|
|
|
|
/// <summary>
|
|
/// The virtual path to the cached file.
|
|
/// </summary>
|
|
private string virtualCachedFilePath;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DiskCache"/> class.
|
|
/// </summary>
|
|
/// <param name="requestPath">
|
|
/// The request path for the image.
|
|
/// </param>
|
|
/// <param name="fullPath">
|
|
/// The full path for the image.
|
|
/// </param>
|
|
/// <param name="querystring">
|
|
/// The querystring containing instructions.
|
|
/// </param>
|
|
public DiskCache(string requestPath, string fullPath, string querystring)
|
|
: base(requestPath, fullPath, querystring)
|
|
{
|
|
this.maxDays = Convert.ToInt32(this.Settings["MaxDays"]);
|
|
string virtualPath = this.Settings["VirtualCachePath"];
|
|
|
|
if (!virtualPath.IsValidVirtualPathName())
|
|
{
|
|
throw new ConfigurationErrorsException("DiskCache 'VirtualCachePath' is not a valid virtual path.");
|
|
}
|
|
|
|
this.virtualCachePath = virtualPath;
|
|
|
|
this.absoluteCachePath = HostingEnvironment.MapPath(this.virtualCachePath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the maximum number of days to store the image.
|
|
/// </summary>
|
|
public override int MaxDays
|
|
{
|
|
get
|
|
{
|
|
return this.maxDays;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the image is new or updated in an asynchronous manner.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// The <see cref="Task"/>.
|
|
/// </returns>
|
|
public override async Task<bool> 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)
|
|
{
|
|
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
|
|
{
|
|
// Check to see if the cached image is set to expire.
|
|
if (this.IsExpired(cachedImage.CreationTimeUtc))
|
|
{
|
|
CacheIndexer.Remove(this.CachedPath);
|
|
isUpdated = true;
|
|
}
|
|
}
|
|
|
|
return isUpdated;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the image to the cache in an asynchronous manner.
|
|
/// </summary>
|
|
/// <param name="stream">
|
|
/// The stream containing the image data.
|
|
/// </param>
|
|
/// <param name="contentType">
|
|
/// The content type of the image.
|
|
/// </param>
|
|
/// <returns>
|
|
/// The <see cref="Task"/> representing an asynchronous operation.
|
|
/// </returns>
|
|
public override async Task AddImageToCacheAsync(Stream stream, string contentType)
|
|
{
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trims the cache of any expired items in an asynchronous manner.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// The asynchronous <see cref="Task"/> representing an asynchronous operation.
|
|
/// </returns>
|
|
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<FileInfo> files = enumerateDirectory.EnumerateFiles().OrderBy(f => f.CreationTimeUtc);
|
|
int count = files.Count();
|
|
|
|
foreach (FileInfo fileInfo in files)
|
|
{
|
|
try
|
|
{
|
|
// If the group count is equal to the max count minus 1 then we know we
|
|
// have reduced the number of items below the maximum allowed.
|
|
// We'll cleanup any orphaned expired files though.
|
|
if (!this.IsExpired(fileInfo.CreationTimeUtc) && count <= MaxFilesCount - 1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Remove from the cache and delete each CachedImage.
|
|
CacheIndexer.Remove(fileInfo.Name);
|
|
fileInfo.Delete();
|
|
count -= 1;
|
|
}
|
|
// ReSharper disable once EmptyGeneralCatchClause
|
|
catch
|
|
{
|
|
// Do nothing; skip to the next file.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rewrites the path to point to the cached image.
|
|
/// </summary>
|
|
/// <param name="context">
|
|
/// The <see cref="HttpContext"/> encapsulating all information about the request.
|
|
/// </param>
|
|
public override void RewritePath(HttpContext context)
|
|
{
|
|
// The cached file is valid so just rewrite the path.
|
|
context.RewritePath(this.virtualCachedFilePath, false);
|
|
}
|
|
}
|
|
}
|
|
|