mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-01-19 17:51:45 +08:00
Fixing the FileSystemOutputCache feature (#7913)
Fixes #8004 #6115 - Caching keys for filenames to prevent too long paths - Separating metadata from content storage to optimize some scenarios
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Orchard.Caching;
|
using Orchard.Caching;
|
||||||
using Orchard.Environment.Configuration;
|
using Orchard.Environment.Configuration;
|
||||||
using Orchard.Environment.Extensions;
|
using Orchard.Environment.Extensions;
|
||||||
@@ -18,7 +19,8 @@ namespace Orchard.OutputCache.Services {
|
|||||||
private readonly IClock _clock;
|
private readonly IClock _clock;
|
||||||
private readonly ISignals _signals;
|
private readonly ISignals _signals;
|
||||||
|
|
||||||
private string _root;
|
private string _content;
|
||||||
|
private string _metadata;
|
||||||
|
|
||||||
public FileSystemOutputCacheBackgroundTask(
|
public FileSystemOutputCacheBackgroundTask(
|
||||||
IAppDataFolder appDataFolder,
|
IAppDataFolder appDataFolder,
|
||||||
@@ -32,22 +34,26 @@ namespace Orchard.OutputCache.Services {
|
|||||||
_clock = clock;
|
_clock = clock;
|
||||||
_signals = signals;
|
_signals = signals;
|
||||||
|
|
||||||
_root = _appDataFolder.Combine("OutputCache", _shellSettings.Name);
|
_metadata = FileSystemOutputCacheStorageProvider.GetMetadataPath(appDataFolder, _shellSettings.Name);
|
||||||
|
_content = FileSystemOutputCacheStorageProvider.GetContentPath(appDataFolder, _shellSettings.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Sweep() {
|
public void Sweep() {
|
||||||
foreach(var filename in _appDataFolder.ListFiles(_root).ToArray()) {
|
foreach(var filename in _appDataFolder.ListFiles(_metadata).ToArray()) {
|
||||||
var validUntilUtc = _cacheManager.Get(filename, context => {
|
var hash = Path.GetFileName(filename);
|
||||||
_signals.When(filename);
|
|
||||||
|
var validUntilUtc = _cacheManager.Get(hash, context => {
|
||||||
|
_signals.When(hash);
|
||||||
|
|
||||||
using (var stream = _appDataFolder.OpenFile(filename)) {
|
using (var stream = _appDataFolder.OpenFile(filename)) {
|
||||||
var cacheItem = FileSystemOutputCacheStorageProvider.Deserialize(stream);
|
var cacheItem = FileSystemOutputCacheStorageProvider.DeserializeMetadata(stream);
|
||||||
return cacheItem.ValidUntilUtc;
|
return cacheItem.ValidUntilUtc;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_clock.UtcNow > validUntilUtc) {
|
if (_clock.UtcNow > validUntilUtc) {
|
||||||
_appDataFolder.DeleteFile(filename);
|
_appDataFolder.DeleteFile(_appDataFolder.Combine(_metadata, hash));
|
||||||
|
_appDataFolder.DeleteFile(_appDataFolder.Combine(_content, hash));
|
||||||
_signals.Trigger(filename);
|
_signals.Trigger(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,51 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Orchard.OutputCache.Models;
|
|
||||||
using Orchard.Environment.Extensions;
|
|
||||||
using Orchard.Logging;
|
|
||||||
using Orchard.Services;
|
|
||||||
using Orchard.FileSystems.AppData;
|
|
||||||
using Orchard.Environment.Configuration;
|
|
||||||
using System.Web;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.Serialization.Formatters.Binary;
|
using System.Runtime.Serialization.Formatters.Binary;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Orchard.Environment.Configuration;
|
||||||
|
using Orchard.Environment.Extensions;
|
||||||
|
using Orchard.FileSystems.AppData;
|
||||||
|
using Orchard.Logging;
|
||||||
|
using Orchard.OutputCache.Models;
|
||||||
|
using Orchard.Services;
|
||||||
|
|
||||||
namespace Orchard.OutputCache.Services {
|
namespace Orchard.OutputCache.Services {
|
||||||
[OrchardFeature("Orchard.OutputCache.FileSystem")]
|
[OrchardFeature("Orchard.OutputCache.FileSystem")]
|
||||||
[OrchardSuppressDependency("Orchard.OutputCache.Services.DefaultCacheStorageProvider")]
|
[OrchardSuppressDependency("Orchard.OutputCache.Services.DefaultCacheStorageProvider")]
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This class provides an implementation of <see cref="IOutputCacheStorageProvider"/>
|
/// This class provides an implementation of <see cref="IOutputCacheStorageProvider"/>
|
||||||
/// based on the local App_Data folder, inside <c>OuputCache/{tenant}</c>. It is not
|
/// based on the local App_Data folder, inside <c>FileCache/{tenant}</c>. It is not
|
||||||
/// recommended when used in a server farm.
|
/// recommended when used in a server farm, unless the file system is share (Azure App Services).
|
||||||
/// The <see cref="CacheItem"/> instances are binary serialized.
|
/// The <see cref="CacheItem"/> instances are binary serialized.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This provider doesn't implement quotas support yet.
|
/// This provider doesn't support quotas yet.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class FileSystemOutputCacheStorageProvider : IOutputCacheStorageProvider {
|
public class FileSystemOutputCacheStorageProvider : IOutputCacheStorageProvider {
|
||||||
private readonly IClock _clock;
|
private readonly IClock _clock;
|
||||||
private readonly IAppDataFolder _appDataFolder;
|
private readonly IAppDataFolder _appDataFolder;
|
||||||
private readonly ShellSettings _shellSettings;
|
private readonly ShellSettings _shellSettings;
|
||||||
private readonly string _root;
|
private readonly string _metadata;
|
||||||
|
private readonly string _content;
|
||||||
|
|
||||||
|
public static char[] InvalidPathChars = { '/', '\\', ':', '*', '?', '>', '<', '|' };
|
||||||
|
|
||||||
public FileSystemOutputCacheStorageProvider(IClock clock, IAppDataFolder appDataFolder, ShellSettings shellSettings) {
|
public FileSystemOutputCacheStorageProvider(IClock clock, IAppDataFolder appDataFolder, ShellSettings shellSettings) {
|
||||||
_appDataFolder = appDataFolder;
|
_appDataFolder = appDataFolder;
|
||||||
_clock = clock;
|
_clock = clock;
|
||||||
_shellSettings = shellSettings;
|
_shellSettings = shellSettings;
|
||||||
_root = _appDataFolder.Combine("OutputCache", _shellSettings.Name);
|
|
||||||
|
_metadata = GetMetadataPath(appDataFolder, _shellSettings.Name);
|
||||||
|
_content = GetContentPath(appDataFolder, _shellSettings.Name);
|
||||||
|
|
||||||
Logger = NullLogger.Instance;
|
Logger = NullLogger.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILogger Logger { get; set; }
|
public ILogger Logger { get; set; }
|
||||||
|
|
||||||
public void Set(string key, CacheItem cacheItem) {
|
public void Set(string key, CacheItem cacheItem) {
|
||||||
Retry(() => {
|
Retry(() => {
|
||||||
if (cacheItem == null) {
|
if (cacheItem == null) {
|
||||||
@@ -50,82 +56,170 @@ namespace Orchard.OutputCache.Services {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filename = GetCacheItemFilename(key);
|
var hash = GetCacheItemFileHash(key);
|
||||||
|
|
||||||
using (var stream = Serialize(cacheItem)) {
|
lock (String.Intern(hash)) {
|
||||||
using (var fileStream = _appDataFolder.CreateFile(filename)) {
|
using (var stream = SerializeContent(cacheItem)) {
|
||||||
stream.CopyTo(fileStream);
|
var filename = _appDataFolder.Combine(_content, hash);
|
||||||
|
using (var fileStream = _appDataFolder.CreateFile(filename)) {
|
||||||
|
stream.CopyTo(fileStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var stream = SerializeMetadata(cacheItem)) {
|
||||||
|
var filename = _appDataFolder.Combine(_metadata, hash);
|
||||||
|
using (var fileStream = _appDataFolder.CreateFile(filename)) {
|
||||||
|
stream.CopyTo(fileStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(string key) {
|
public void Remove(string key) {
|
||||||
Retry(() => {
|
var hash = GetCacheItemFileHash(key);
|
||||||
var filename = GetCacheItemFilename(key);
|
lock (String.Intern(hash)) {
|
||||||
if (_appDataFolder.FileExists(filename)) {
|
Retry(() => {
|
||||||
_appDataFolder.DeleteFile(filename);
|
var filename = _appDataFolder.Combine(_metadata, hash);
|
||||||
}
|
if (_appDataFolder.FileExists(filename)) {
|
||||||
});
|
_appDataFolder.DeleteFile(filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Retry(() => {
|
||||||
|
var filename = _appDataFolder.Combine(_content, hash);
|
||||||
|
if (_appDataFolder.FileExists(filename)) {
|
||||||
|
_appDataFolder.DeleteFile(filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveAll() {
|
public void RemoveAll() {
|
||||||
foreach(var filename in _appDataFolder.ListFiles(_root)) {
|
foreach (var folder in new[] { _metadata, _content }) {
|
||||||
if(_appDataFolder.FileExists(filename)) {
|
foreach (var filename in _appDataFolder.ListFiles(folder)) {
|
||||||
_appDataFolder.DeleteFile(filename);
|
var hash = Path.GetFileName(filename);
|
||||||
|
lock (String.Intern(hash)) {
|
||||||
|
try {
|
||||||
|
if (_appDataFolder.FileExists(filename)) {
|
||||||
|
_appDataFolder.DeleteFile(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Logger.Warning(e, "An error occured while deleting the file: {0}", filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CacheItem GetCacheItem(string key) {
|
public CacheItem GetCacheItem(string key) {
|
||||||
return Retry(() => {
|
return Retry(() => {
|
||||||
var filename = GetCacheItemFilename(key);
|
var hash = GetCacheItemFileHash(key);
|
||||||
|
lock (String.Intern(hash)) {
|
||||||
|
var filename = _appDataFolder.Combine(_metadata, hash);
|
||||||
|
|
||||||
if (!_appDataFolder.FileExists(filename)) {
|
if (!_appDataFolder.FileExists(filename)) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var stream = _appDataFolder.OpenFile(filename)) {
|
|
||||||
|
|
||||||
if (stream == null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Deserialize(stream);
|
CacheItem cacheItem = null;
|
||||||
|
|
||||||
|
using (var stream = _appDataFolder.OpenFile(filename)) {
|
||||||
|
if (stream == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheItem = DeserializeMetadata(stream);
|
||||||
|
|
||||||
|
// We compare the requested key and the one stored in the metadata
|
||||||
|
// as there could be key collisions with the hashed filenames.
|
||||||
|
if (!cacheItem.CacheKey.Equals(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = _appDataFolder.Combine(_content, hash);
|
||||||
|
using (var stream = _appDataFolder.OpenFile(filename)) {
|
||||||
|
if (stream == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = DeserializeContent(stream);
|
||||||
|
cacheItem.Output = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheItem;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<CacheItem> GetCacheItems(int skip, int count) {
|
public IEnumerable<CacheItem> GetCacheItems(int skip, int count) {
|
||||||
return _appDataFolder.ListFiles(_root)
|
return _appDataFolder.ListFiles(_metadata)
|
||||||
.OrderBy(x => x)
|
.OrderBy(x => x)
|
||||||
.Skip(skip)
|
.Skip(skip)
|
||||||
.Take(count)
|
.Take(count)
|
||||||
.Select(filename => {
|
.Select(filename => {
|
||||||
using (var stream = _appDataFolder.OpenFile(filename)) {
|
using (var stream = _appDataFolder.OpenFile(filename)) {
|
||||||
return Deserialize(stream);
|
return DeserializeMetadata(stream);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetCacheItemsCount() {
|
public int GetCacheItemsCount() {
|
||||||
return _appDataFolder.ListFiles(_root).Count();
|
return _appDataFolder.ListFiles(_metadata).Count();
|
||||||
}
|
|
||||||
|
|
||||||
private string GetCacheItemFilename(string key) {
|
|
||||||
return _appDataFolder.Combine(_root, HttpUtility.UrlEncode(key));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static MemoryStream Serialize(CacheItem item) {
|
public static string GetMetadataPath(IAppDataFolder appDataFolder, string tenant) {
|
||||||
|
return appDataFolder.Combine("FileCache", tenant, "metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetContentPath(IAppDataFolder appDataFolder, string tenant) {
|
||||||
|
return appDataFolder.Combine("FileCache", tenant, "content");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCacheItemFileHash(string key) {
|
||||||
|
// The key is typically too long to be useful, so we use a hash
|
||||||
|
using (var md5 = MD5.Create()) {
|
||||||
|
var keyBytes = Encoding.UTF8.GetBytes(key);
|
||||||
|
var hashedBytes = md5.ComputeHash(keyBytes);
|
||||||
|
var b64 = Convert.ToBase64String(hashedBytes);
|
||||||
|
return String.Join("-", b64.Split(InvalidPathChars, StringSplitOptions.RemoveEmptyEntries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MemoryStream SerializeContent(CacheItem item) {
|
||||||
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
binaryFormatter.Serialize(memoryStream, item);
|
binaryFormatter.Serialize(memoryStream, item.Output);
|
||||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
return memoryStream;
|
return memoryStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static CacheItem Deserialize(Stream stream) {
|
internal static byte[] DeserializeContent(Stream stream) {
|
||||||
|
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
||||||
|
var result = (byte[])binaryFormatter.Deserialize(stream);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MemoryStream SerializeMetadata(CacheItem item) {
|
||||||
|
var output = item.Output;
|
||||||
|
item.Output = new byte[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
binaryFormatter.Serialize(memoryStream, item);
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
return memoryStream;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
item.Output = output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CacheItem DeserializeMetadata(Stream stream) {
|
||||||
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
||||||
var result = (CacheItem)binaryFormatter.Deserialize(stream);
|
var result = (CacheItem)binaryFormatter.Deserialize(stream);
|
||||||
return result;
|
return result;
|
||||||
@@ -138,7 +232,9 @@ namespace Orchard.OutputCache.Services {
|
|||||||
var t = action();
|
var t = action();
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
catch {
|
catch(Exception e) {
|
||||||
|
Logger.Warning("An unexpected error occured, attempt # {0}, i", e);
|
||||||
|
|
||||||
if (i == retries) {
|
if (i == retries) {
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -153,9 +249,12 @@ namespace Orchard.OutputCache.Services {
|
|||||||
for(int i=1; i <= retries; i++) {
|
for(int i=1; i <= retries; i++) {
|
||||||
try {
|
try {
|
||||||
action();
|
action();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch {
|
catch(Exception e) {
|
||||||
if(i == retries) {
|
Logger.Warning("An unexpected error occured, attempt # {0}, i", e);
|
||||||
|
|
||||||
|
if (i == retries) {
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user