using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace FlickrNet
{
///
/// A threadsafe cache that is backed by disk storage.
///
/// All public methods that read or write state must be
/// protected by the lockFile. Private methods should
/// not acquire the lockFile as it is not reentrant.
///
internal sealed class PersistentCache
{
// The in-memory representation of the cache.
// Use SortedList instead of Hashtable only to maintain backward
// compatibility with previous serialization scheme. If we
// abandon backward compatibility, we should switch to Hashtable.
private Hashtable dataTable = new Hashtable();
private readonly CacheItemPersister persister;
// true if dataTable contains changes vs. on-disk representation
private bool dirty;
// The persistent file representation of the cache.
private readonly FileInfo dataFile;
private DateTime timestamp; // last-modified time of dataFile when cache data was last known to be in sync
private long length; // length of dataFile when cache data was last known to be in sync
private long maxSize;
// The file-based mutex. Named (dataFile.FullName + ".lock")
private readonly LockFile lockFile;
public PersistentCache(string filename, CacheItemPersister persister, long maxSize)
{
this.maxSize = maxSize;
this.persister = persister;
this.dataFile = new FileInfo(filename);
this.lockFile = new LockFile(filename + ".lock");
}
///
/// Return all items in the cache. Works similarly to
/// ArrayList.ToArray(Type).
///
public ICacheItem[] ToArray(Type valueType)
{
using (lockFile.Acquire())
{
Refresh();
string[] keys;
Array values;
InternalGetAll(valueType, out keys, out values);
return (ICacheItem[]) values;
}
}
public int Count
{
get
{
using (lockFile.Acquire())
{
Refresh();
return dataTable.Count;
}
}
}
///
/// Gets the maximum size for the persistent cache.
///
public long MaxSize
{
get { return maxSize; }
}
///
/// Gets or sets cache values.
///
public ICacheItem this[string key]
{
get
{
if (key == null)
throw new ArgumentNullException("key");
using (lockFile.Acquire())
{
Refresh();
return InternalGet(key);
}
}
set
{
if (key == null)
throw new ArgumentNullException("key");
ICacheItem oldItem;
using (lockFile.Acquire())
{
Refresh();
oldItem = InternalSet(key, value);
Persist();
}
if (oldItem != null)
oldItem.OnItemFlushed();
}
}
public ICacheItem Get(string key, TimeSpan maxAge, bool removeIfExpired)
{
Debug.Assert(maxAge > TimeSpan.Zero || maxAge == TimeSpan.MinValue, "maxAge should be positive, not negative");
ICacheItem item;
bool expired;
using (lockFile.Acquire())
{
Refresh();
item = InternalGet(key);
expired = item != null && Expired(item.CreationTime, maxAge);
if (expired)
{
if (removeIfExpired)
{
item = RemoveKey(key);
Persist();
}
else
item = null;
}
}
if (expired && removeIfExpired)
item.OnItemFlushed();
return expired ? null : item;
}
public void Flush()
{
Shrink(0);
}
public void Shrink(long size)
{
if (size < 0)
throw new ArgumentException("Cannot shrink to a negative size", "size");
ArrayList flushed = new ArrayList();
using (lockFile.Acquire())
{
Refresh();
string[] keys;
Array values;
InternalGetAll(typeof(ICacheItem), out keys, out values);
long totalSize = 0;
foreach (ICacheItem cacheItem in values)
totalSize += cacheItem.FileSize;
for (int i = 0; i < keys.Length; i++)
{
if (totalSize <= size)
break;
ICacheItem cacheItem = (ICacheItem) values.GetValue(i);
totalSize -= cacheItem.FileSize;
flushed.Add(RemoveKey(keys[i]));
}
Persist();
}
foreach (ICacheItem flushedItem in flushed)
{
Debug.Assert(flushedItem != null, "Flushed item was null--programmer error");
if (flushedItem != null)
flushedItem.OnItemFlushed();
}
}
private bool Expired(DateTime test, TimeSpan age)
{
if (age == TimeSpan.MinValue)
return true;
else if (age == TimeSpan.MaxValue)
return false;
else
return test < DateTime.UtcNow - age;
}
private void InternalGetAll(Type valueType, out string[] keys, out Array values)
{
if (!typeof(ICacheItem).IsAssignableFrom(valueType))
throw new ArgumentException("Type " + valueType.FullName + " does not implement ICacheItem", "valueType");
keys = (string[]) new ArrayList(dataTable.Keys).ToArray(typeof(string));
values = Array.CreateInstance(valueType, keys.Length);
for (int i = 0; i < keys.Length; i++)
values.SetValue(dataTable[keys[i]], i);
Array.Sort(values, keys, new CreationTimeComparer());
}
private ICacheItem InternalGet(string key)
{
if (key == null)
throw new ArgumentNullException("key");
return (ICacheItem) dataTable[key];
}
/// The old value associated with key, if any.
private ICacheItem InternalSet(string key, ICacheItem value)
{
if (key == null)
throw new ArgumentNullException("key");
ICacheItem flushedItem;
flushedItem = RemoveKey(key);
if (value != null) // don't ever let nulls get in
dataTable[key] = value;
dirty = dirty || !object.ReferenceEquals(flushedItem, value);
return flushedItem;
}
private ICacheItem RemoveKey(string key)
{
ICacheItem cacheItem = (ICacheItem) dataTable[key];
if (cacheItem != null)
{
dataTable.Remove(key);
dirty = true;
}
return cacheItem;
}
private void Refresh()
{
Debug.Assert(!dirty, "Refreshing even though cache is dirty");
DateTime newTimestamp = DateTime.MinValue;
long newLength = -1;
if (dataFile.Exists)
{
dataFile.Refresh();
newTimestamp = dataFile.LastWriteTimeUtc;
newLength = dataFile.Length;
}
if (timestamp != newTimestamp || length != newLength)
{
// file changed
if (!dataFile.Exists)
dataTable.Clear();
else
{
Debug.WriteLine("Loading cache from disk");
using (FileStream inStream = dataFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
{
dataTable = Load(inStream);
}
}
}
timestamp = newTimestamp;
length = newLength;
dirty = false;
}
private void Persist()
{
if (!dirty)
return;
Debug.WriteLine("Saving cache to disk");
using (FileStream outStream = dataFile.Open(FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
{
Store(outStream, dataTable);
}
dataFile.Refresh();
timestamp = dataFile.LastWriteTimeUtc;
length = dataFile.Length;
dirty = false;
}
private Hashtable Load(Stream s)
{
Hashtable table = new Hashtable();
int itemCount = Utils.ReadInt32(s);
for (int i = 0; i < itemCount; i++)
{
try
{
string key = Utils.ReadString(s);
ICacheItem val = persister.Read(s);
if( val == null ) // corrupt cache file
return table;
table[key] = val;
}
catch(IOException)
{
return table;
}
}
return table;
}
private void Store(Stream s, Hashtable table)
{
Utils.WriteInt32(s, table.Count);
foreach (DictionaryEntry entry in table)
{
Utils.WriteString(s, (string) entry.Key);
persister.Write(s, (ICacheItem) entry.Value);
}
}
private class CreationTimeComparer : IComparer
{
public int Compare(object x, object y)
{
return ((ICacheItem)x).CreationTime.CompareTo(((ICacheItem)y).CreationTime);
}
}
}
}