Browse Source

Initial wire up of Azure blob cache

TODO: Error checking with container urls.

Former-commit-id: 268e3adea98e2d485fea36565e8e804921b1144e
Former-commit-id: b75f385b341d4d66c6438db0ebf6b3cded0039c7
af/merge-core
James South 11 years ago
parent
commit
552267a0f5
  1. 8
      src/ImageProcessor.Playground/ImageProcessor.Playground.csproj
  2. 5
      src/ImageProcessor.Playground/Program.cs
  3. 129
      src/ImageProcessor.Web.AzureBlobCache/AzureBlobCache.cs
  4. 4
      src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj
  5. 5
      src/ImageProcessor.Web.PostProcessor/PostProcessor.cs
  6. 17
      src/ImageProcessor.Web/Caching/CacheIndexer.cs
  7. 2
      src/ImageProcessor.Web/Configuration/ImageCacheSection.cs
  8. 28
      src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs
  9. 18
      src/TestWebsites/MVC/Views/Home/Index.cshtml
  10. 4
      src/TestWebsites/MVC/Web.config

8
src/ImageProcessor.Playground/ImageProcessor.Playground.csproj

@ -55,6 +55,14 @@
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ImageProcessor.Web.AzureBlobCache\ImageProcessor.Web.AzureBlobCache.csproj">
<Project>{3c805e4c-d679-43f8-8c43-8909cdb4d4d7}</Project>
<Name>ImageProcessor.Web.AzureBlobCache</Name>
</ProjectReference>
<ProjectReference Include="..\ImageProcessor.Web\ImageProcessor.Web.csproj">
<Project>{d011a778-59c8-4bfa-a770-c350216bf161}</Project>
<Name>ImageProcessor.Web</Name>
</ProjectReference>
<ProjectReference Include="..\ImageProcessor\ImageProcessor.csproj">
<Project>{3B5DD734-FB7A-487D-8CE6-55E7AF9AEA7E}</Project>
<Name>ImageProcessor</Name>

5
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;
/// <summary>
/// The program.
@ -38,6 +39,10 @@ namespace ImageProcessor.PlayGround
/// </param>
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

129
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;
/// <summary>
/// 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);
}
}
}
}

4
src/ImageProcessor.Web.AzureBlobCache/ImageProcessor.Web.AzureBlobCache.csproj

@ -7,8 +7,8 @@
<ProjectGuid>{3C805E4C-D679-43F8-8C43-8909CDB4D4D7}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ImageProcessor.Web.AzureBlobCache</RootNamespace>
<AssemblyName>ImageProcessor.Web.AzureBlobCache</AssemblyName>
<RootNamespace>ImageProcessor.Web.Caching</RootNamespace>
<AssemblyName>ImageProcessor.Web.Caching.AzureBlobCache</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>

5
src/ImageProcessor.Web.PostProcessor/PostProcessor.cs

@ -34,6 +34,11 @@ namespace ImageProcessor.Web.PostProcessor
/// </returns>
public static async Task PostProcessImageAsync(string sourceFile)
{
if (!new Uri(sourceFile).IsFile)
{
return;
}
string targetFile = Path.GetTempFileName();
PostProcessingResultEventArgs result = await RunProcess(sourceFile, targetFile);

17
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
/// </returns>
public static CachedImage Add(CachedImage cachedImage)
{
// Add the CachedImage.
CacheItemPolicy policy = new CacheItemPolicy();
policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string> { cachedImage.Path }));
if (new Uri(cachedImage.Path).IsFile)
{
// Add the CachedImage.
CacheItemPolicy policy = new CacheItemPolicy();
policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string> { 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

2
src/ImageProcessor.Web/Configuration/ImageCacheSection.cs

@ -59,7 +59,7 @@ namespace ImageProcessor.Web.Configuration
/// <returns>The cache configuration section from the current application configuration.</returns>
public static ImageCacheSection GetConfiguration()
{
ImageCacheSection imageCacheSection = ConfigurationManager.GetSection("imageProcessor/cache") as ImageCacheSection;
ImageCacheSection imageCacheSection = ConfigurationManager.GetSection("imageProcessor/caching") as ImageCacheSection;
if (imageCacheSection != null)
{

28
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<string> { this.imageCache.CachedPath }
: new List<string> { requestPath, this.imageCache.CachedPath };
}
else
{
context.Items[CachedResponseFileDependency] = new List<string> { this.imageCache.CachedPath };
}
}
// The cached file is valid so just rewrite the path.
this.imageCache.RewritePath(context);
}

18
src/TestWebsites/MVC/Views/Home/Index.cshtml

@ -7,24 +7,24 @@
<div class="row">
<div class="col-s-6">
<h2>Resized</h2>
<img src="/images/format-Penguins.jpg?width=300" />
<h3>Foreign language test.</h3>
<img src="/images/format-Penguins.jpg?width=305" />
@*<h3>Foreign language test.</h3>
<img src="/images/udendørs.jpg?width=300" />
<img src="/images/udendørs.jpg?preset=demo&filter=comic" />
<h3>Strange name</h3>
<img src="/images/falahill_design__160p.jpg?preset=demo" />
<img src="/images/falahill_design__160p.jpg?preset=demo" />*@
</div>
<div class="col-s-6">
@*<div class="col-s-6">
<h2>Cropped </h2>
<img src="/images/format-Penguins.jpg?crop=0,0,300,225" />
<h3>Cropped Percent</h3>
<img src="/images/format-Penguins.jpg?width=300&height300&crop=0.1,0.2,0.1,0.6&cropmode=percent" />
<img src="/images/bus.jpg?width=311" />
</div>
</div>*@
</div>
</section>
<section>
@*<section>
<div class="row">
<h2>Reside Pad</h2>
<div class="col-s-4">
@ -199,9 +199,9 @@
<img src="/images/format-Penguins.jpg?width=300&tint=af76a6" />
</div>
</div>
</section>
</section>*@
</article>
<article>
@*<article>
<h1>Color Profiles</h1>
<section>
<div class="row">
@ -223,4 +223,4 @@
<img src="/Images/header_1.jpg?width=750&crop=0-48-750-220" />
</div>
</section>
</article>
</article>*@

4
src/TestWebsites/MVC/Web.config

@ -9,12 +9,12 @@
<sectionGroup name="imageProcessor">
<section name="security" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageSecuritySection, ImageProcessor.Web" />
<section name="processing" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageProcessingSection, ImageProcessor.Web" />
<section name="cache" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageCacheSection, ImageProcessor.Web" />
<section name="caching" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageCacheSection, ImageProcessor.Web" />
</sectionGroup>
</configSections>
<imageProcessor>
<security configSource="config\imageprocessor\security.config" />
<cache configSource="config\imageprocessor\cache.config" />
<caching configSource="config\imageprocessor\cache.config" />
<processing configSource="config\imageprocessor\processing.config" />
</imageProcessor>
<appSettings>

Loading…
Cancel
Save