using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Security.Principal;
using System.Threading;
using Pfz.Extensions.DisposeExtensions;
using Pfz.Threading;
namespace Pfz.Caching
{
/// <summary>
/// The default persister used by CacheManager.
/// </summary>
public sealed class FileCachePersister:
ICachePersister
{
/// <summary>
/// Creates the FileCachePersister using the given directory as it's working directory.
/// CAUTION - All files and this directory will eventually be deleted.
/// </summary>
public FileCachePersister(string path):
this(path, new TimeSpan(0, 30, 0), new TimeSpan(4, 0, 0), WindowsIdentity.GetCurrent())
{
}
/// <summary>
/// Creates the FileCachePersister with all parameters.
/// </summary>
public FileCachePersister(string path, TimeSpan cleaningInterval, TimeSpan cleanFilesOlderThan, WindowsIdentity cleaningThreadImpersonation)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException("path", "If you want to use the current directory, use a dot \".\".");
fCleanFilesOlderThan = cleanFilesOlderThan;
fCleaningInterval = cleaningInterval;
fCleaningThreadImpersonation = cleaningThreadImpersonation;
if (path[path.Length-1] != '\\')
path += '\\';
BasePath = path;
BuffersPath = path + "Buffers\\";
ViewStatesPath = path + "ViewStates\\";
if (fCleanFilesOlderThan < fCleaningInterval)
throw new CacheException("CleanFilesOlderThan must be at least equal to CleaningInterval");
if (fCleaningThreadImpersonation == null)
fCleaningThreadImpersonation = WindowsIdentity.GetCurrent(false);
if (fCleaningInterval != TimeSpan.Zero)
{
Thread cleanerThread = new Thread(p_Clean);
cleanerThread.Name = "Pfz.Caching.FileCachePersister.CleanerThread - cleans unused cache files";
cleanerThread.Priority = ThreadPriority.Lowest;
cleanerThread.IsBackground = true;
cleanerThread.Start();
}
}
#region Properties
#region CleaningInterval
private static TimeSpan fCleaningInterval;
/// <summary>
/// Gets the time in which cleaning thread executes to delete old files. This is
/// the interval between executions, not how old files can be.
/// The default interval is 30 minutes.
/// If CleaningInterval is set to Zero, the automatic cleaning will
/// never be executed. Set it to Zero only if you do your own cleaning.
/// </summary>
public static TimeSpan CleaningInterval
{
get
{
return fCleaningInterval;
}
}
#endregion
#region CleanFilesOlderThan
private static TimeSpan fCleanFilesOlderThan;
/// <summary>
/// Gets or sets how old files can be and kept alive. Older will be deleted at the
/// next execution of Cleaner Thread.
/// The default value is 4 hours.
/// </summary>
public static TimeSpan CleanFilesOlderThan
{
get
{
return fCleanFilesOlderThan;
}
}
#endregion
#region CleaningThreadImpersonation
private static WindowsIdentity fCleaningThreadImpersonation;
/// <summary>
/// Gets or sets the impersonation used by the cleaning thread.
/// If this value is null, the impersonation of the thread that
/// calls Start() will be used.
/// </summary>
public static WindowsIdentity CleaningThreadImpersonation
{
get
{
return fCleaningThreadImpersonation;
}
}
#endregion
/// <summary>
/// Gets the path used to create this FileCachePersister.
/// </summary>
public string BasePath { get; private set; }
/// <summary>
/// Gets the path used by this FileCachePersister to store it's cache buffers.
/// </summary>
public string BuffersPath { get; private set; }
/// <summary>
/// Gets the path used by this FileCachePersister to store it's viewstates.
/// </summary>
public string ViewStatesPath { get; private set; }
#endregion
#region Buffers
/// <summary>
/// Reads the buffer using the hashCode as a directory and the id as the filename.
/// </summary>
/// <returns>The read buffer or null.</returns>
public byte[] GetBuffer(long hashCode, long id)
{
string filePath = string.Format("{0}{1}\\{2}", BuffersPath, hashCode, id);
if (!File.Exists(filePath))
return null;
try
{
FileStream stream = null;
try
{
AbortSafe.Run(()=> stream = File.OpenRead(filePath));
int length = (int)stream.Length;
byte[] bytes = new byte[length];
if (stream.Read(bytes, 0, length) != length)
return null;
return bytes;
}
finally
{
stream.CheckedDispose();
}
}
catch
{
return null;
}
}
/// <summary>
/// Saves the buffer using the hashCode as a sub-directory and the id as the file name.
/// </summary>
public long SaveBuffer(byte[] buffer, long hashCode)
{
if (buffer == null)
throw new ArgumentNullException("buffer");
int length = buffer.Length;
long id;
string directoryPath = BuffersPath + hashCode;
DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
if (directoryInfo.Exists)
{
try
{
FileInfo[] files = directoryInfo.GetFiles();
byte[] otherBuffer = null;
foreach(FileInfo file in files)
{
if (file.Length != length)
continue;
try
{
if (otherBuffer == null)
otherBuffer = new byte[length];
FileStream stream = null;
try
{
AbortSafe.Run(()=>stream = file.OpenRead());
if (stream.Read(otherBuffer, 0, length) != length)
continue;
if (buffer.SequenceEqual(otherBuffer))
{
id = long.Parse(file.Name);
return id;
}
}
finally
{
stream.CheckedDispose();
}
}
catch
{
}
}
}
catch
{
}
}
id = TicksOrIncrement.GetNext();
string path = directoryPath + '\\' + id;
Directory.CreateDirectory(directoryPath);
AbortSafe.WriteAllBytes(path, buffer);
return id;
}
/// <summary>
/// Resets the file LastWriteTime.
/// </summary>
public bool KeepBufferAlive(long hashCode, long id)
{
try
{
string path = string.Format("{0}{1}\\{2}", BuffersPath, hashCode, id);
if (!File.Exists(path))
return false;
File.SetLastWriteTime(path, DateTime.Now);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Recreates a buffer that is still in memory, but was deleted.
/// </summary>
public void RecreateBuffer(long hashCode, long id, byte[] buffer)
{
string directoryPath = BuffersPath + hashCode;
Directory.CreateDirectory(directoryPath);
string filePath = directoryPath + '\\' + id;
AbortSafe.WriteAllBytes(filePath, buffer);
}
#endregion
#region ViewStates
/// <summary>
/// Gets a view state.
/// </summary>
public byte[] GetViewState(string sessionId, long viewId)
{
try
{
string path = string.Format("{0}{1}\\{2}", ViewStatesPath, sessionId, viewId);
return AbortSafe.ReadAllBytes(path);
}
catch
{
return null;
}
}
/// <summary>
/// Saves a view state.
/// </summary>
public long SaveViewState(string sessionId, byte[] bytes)
{
string directoryPath = ViewStatesPath + sessionId;
Directory.CreateDirectory(directoryPath);
long viewId = TicksOrIncrement.GetNext();
string path = string.Format("{0}\\{1}", directoryPath, viewId);
AbortSafe.WriteAllBytes(path, bytes);
return viewId;
}
/// <summary>
/// Resets the file LastWriteTime.
/// </summary>
/// <param name="sessionId"></param>
/// <param name="key"></param>
/// <returns></returns>
public bool KeepViewStateAlive(string sessionId, long key)
{
try
{
string path = string.Format("{0}{1}\\{2}", ViewStatesPath, sessionId, key);
if (!File.Exists(path))
return false;
File.SetLastWriteTime(path, DateTime.Now);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Recreates a given viewstate that is in memory, but deleted.
/// </summary>
/// <param name="sessionId"></param>
/// <param name="key"></param>
/// <param name="bytes"></param>
public void RecreateViewState(string sessionId, long key, byte[] bytes)
{
string directoryPath = ViewStatesPath + sessionId;
Directory.CreateDirectory(directoryPath);
string filePath = directoryPath + '\\' + key;
AbortSafe.WriteAllBytes(filePath, bytes);
}
#endregion
#region Interlocked
private Dictionary<string, long> fInterlocked = new Dictionary<string, long>();
/// <summary>
/// Gets an interlocked variable value or, if one does not exists, create
/// it with the given value, also doing an atomic add immediatelly.
/// </summary>
public long InterlockedGetOrCreateAndAdd(string interlockedName, long value, int addAmount)
{
long result = 0;
AbortSafe.UnabortableLock
(
fInterlocked,
delegate
{
if (!fInterlocked.TryGetValue(interlockedName, out result))
result = value;
result += addAmount;
fInterlocked[interlockedName] = result;
}
);
return result;
}
/// <summary>
/// Adds the given amount to an interlocked variable.
/// Returns null if the variable does not exist.
/// </summary>
public long? InterlockedAdd(string interlockedName, int amount)
{
long? result = null;
AbortSafe.UnabortableLock
(
fInterlocked,
delegate
{
long longResult;
if (!fInterlocked.TryGetValue(interlockedName, out longResult))
return;
longResult += amount;
fInterlocked[interlockedName] = longResult;
result = longResult;
}
);
return result;
}
#endregion
#region p_Clean
private void p_Clean()
{
if (fCleaningThreadImpersonation != null)
fCleaningThreadImpersonation.Impersonate();
while(true)
{
DirectoryInfo directoryInfo = new DirectoryInfo(BasePath);
p_Clean(directoryInfo);
Thread.Sleep(fCleaningInterval);
}
}
private static void p_Clean(DirectoryInfo directoryInfo)
{
try
{
DirectoryInfo[] subDirectories = directoryInfo.GetDirectories();
foreach(DirectoryInfo subDirectory in subDirectories)
p_Clean(subDirectory);
DateTime old = DateTime.Now - fCleanFilesOlderThan;
FileInfo[] files = directoryInfo.GetFiles();
foreach(FileInfo file in files)
{
try
{
if (file.LastWriteTime < old)
file.Delete();
}
catch(SecurityException){}
catch(IOException){}
catch(UnauthorizedAccessException){}
}
directoryInfo.Delete();
}
catch
{
}
}
#endregion
}
}