// --------------------------------------------------------------------------------------------------------------------
//
// 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;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using ImageProcessor.Web.Extensions;
///
/// Provides an implementation that is file system based.
/// The cache is self healing and cleaning.
///
public class DiskCache : 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 maximum number of days to store the image.
///
private readonly int maxDays;
///
/// The virtual cache path.
///
private readonly string virtualCachePath;
///
/// The absolute path to virtual cache path on the server.
///
private readonly string absoluteCachePath;
///
/// The virtual path to the cached file.
///
private string virtualCachedFilePath;
///
/// Initializes a new instance of the class.
///
///
/// The request path for the image.
///
///
/// The full path for the image.
///
///
/// The querystring containing instructions.
///
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);
}
///
/// Gets the maximum number of days to store the image.
///
public override int MaxDays
{
get
{
return this.maxDays;
}
}
///
/// 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();
// 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;
}
///
/// 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
DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(this.CachedPath));
if (!directoryInfo.Exists)
{
directoryInfo.Create();
}
using (FileStream fileStream = File.Create(this.CachedPath))
{
await stream.CopyToAsync(fileStream);
}
}
///
/// 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);
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.
}
}
}
}
}
}
///
/// 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.
context.RewritePath(this.virtualCachedFilePath, false);
}
}
}