00001 using System;
00002 using System.IO;
00003 using System.Text;
00004 using System.Collections;
00005 using System.Collections.Specialized;
00006 using System.Security.Cryptography;
00007 using System.Runtime.Serialization;
00008 using System.Runtime.Serialization.Formatters.Binary;
00009
00010
00011 namespace Common {
00012
00013 [Serializable]
00014 public class CacheIndex : ListDictionary {
00015 private readonly string BASE_PATH;
00016 private readonly string EXT;
00017 private long maxCacheSize;
00018
00019 public CacheIndex(string basePath, string ext, long maxCacheSize) {
00020 this.BASE_PATH = basePath;
00021 this.EXT = ext;
00022 this.maxCacheSize = maxCacheSize;
00023 UpdateFromDisk();
00024 }
00025
00026 public static CacheIndex Deserialise(byte[] srcCompressed) {
00027 MemoryStream compressedStream = new MemoryStream(srcCompressed);
00028 byte[] srcUncompressed = EncodedData.StaticUnzip(ref compressedStream);
00029 compressedStream.Close();
00030 IFormatter formatter = new BinaryFormatter();
00031 return (CacheIndex)formatter.Deserialize(new MemoryStream(srcUncompressed));
00032 }
00033
00034 public byte[] Serialise() {
00035 IFormatter formatter = new BinaryFormatter();
00036 MemoryStream bufferStream = new MemoryStream();
00037 formatter.Serialize(bufferStream, this);
00038 byte[] result = EncodedData.StaticZip(bufferStream);
00039 bufferStream.Close();
00040 return result;
00041 }
00042
00043 private void UpdateFromDisk() {
00044 lock (SyncRoot) {
00045 Console.Write("Populating Cache Index... ");
00046 string[] searchResult = Directory.GetFiles(BASE_PATH, "*" + EXT);
00047 foreach(string filename in searchResult) {
00048 FileStream fs = File.OpenRead(filename);
00049 CacheIndexEntry cie = new CacheIndexEntry(Cache.ChkFromString(Path.GetFileNameWithoutExtension(filename)), Path.GetFileNameWithoutExtension(filename), (int)fs.Length, CacheEntry.DeserialiseDate(fs));
00050 fs.Close();
00051 this.Add(cie);
00052 }
00053 Console.WriteLine("Done.");
00054 }
00055 }
00056 public CacheIndexEntry this[byte[] chk] {
00057 get {
00058 lock(SyncRoot) {
00059 return (CacheIndexEntry)base[Cache.StringFromChk(chk)];
00060 }
00061 }
00062 set {
00063 lock(SyncRoot) {
00064 base[Cache.StringFromChk(chk)] = value;
00065 }
00066 }
00067 }
00068
00069 public bool Contains(byte[] chk){
00070 lock (SyncRoot) {
00071 return this.Contains(Cache.StringFromChk(chk));
00072 }
00073 }
00074
00075 public void Remove(byte[] chk) {
00076 lock (SyncRoot) {
00077 base.Remove(Cache.StringFromChk(chk));
00078 }
00079 }
00080
00081 public void Add(CacheIndexEntry cie) {
00082 lock (SyncRoot) {
00083 this.Add(Cache.StringFromChk(cie.chk), cie);
00084 }
00085 }
00086
00093 public CacheIndexEntry LeastRecentlyUsed() {
00094 lock (SyncRoot) {
00095 CacheIndexEntry evictionCandidate = null;
00096 if (this.Count != 0) {
00097 DateTime minLRU = DateTime.MaxValue;
00098 foreach(object o in this) {
00099 CacheIndexEntry cie = (CacheIndexEntry)((DictionaryEntry)o).Value;
00100 if (cie.lastUsed <= minLRU) {
00101 minLRU = cie.lastUsed;
00102 evictionCandidate = cie;
00103 }
00104 }
00105 }
00106 return evictionCandidate;
00107 }
00108 }
00109
00110 public long GetSpaceRemaining() {
00111 lock (SyncRoot) {
00112 long totalSize = 0;
00113 foreach(object o in this.Values) {
00114 CacheIndexEntry cie = (CacheIndexEntry)o;
00115 totalSize += cie.size;
00116 }
00117 return maxCacheSize - totalSize;
00118 }
00119 }
00120 }
00121
00122
00123 [Serializable]
00124 public class CacheIndexEntry {
00125 public byte[] chk;
00126 public string filename;
00127 public int size;
00128 public DateTime lastUsed;
00129
00130 public CacheIndexEntry(byte[] chk, string filename, int size, DateTime lastUsed) {
00131 this.chk = chk;
00132 this.filename = filename;
00133 this.size = size;
00134 this.lastUsed = lastUsed;
00135 }
00136 }
00137
00138
00139 public class Cache {
00140 CommonSettings settings;
00141 readonly string BASE_PATH;
00142 readonly string EXT;
00143
00144 SHA1Managed sha1;
00145 CacheIndex index;
00146 Object SyncRoot = new Object();
00147 long maxSize;
00148 long spaceUsed;
00149 internal CacheManager cacheManager;
00150
00151 public Cache(long maxSize, CacheManager cacheManager) {
00152 this.cacheManager = cacheManager;
00153 this.maxSize = maxSize;
00154 settings = new CommonSettings();
00155 BASE_PATH = settings.CacheLocation;
00156 EXT = settings.CacheFileExtension;
00157 sha1 = new SHA1Managed();
00158 index = new CacheIndex(BASE_PATH, EXT, maxSize);
00159 spaceUsed = CheckSpaceUsed();
00160 }
00161
00166 long CheckSpaceUsed() {
00167 lock (SyncRoot) {
00168 DirectoryInfo dirInfo = new DirectoryInfo(BASE_PATH);
00169 long totalSize = 0;
00170 FileSystemInfo[] fiArr = dirInfo.GetFileSystemInfos("*" + EXT);
00171 foreach (FileInfo fi in fiArr) {
00172 totalSize += fi.Length;
00173 }
00174 Console.WriteLine("Cache files take up {0} bytes on disk.", totalSize);
00175 return totalSize;
00176 }
00177 }
00178
00184 public byte[] Put(HTTPBody body) {
00185 lock (SyncRoot) {
00186 byte[] chk = null;
00187 if (body != null) {
00188 chk = sha1.ComputeHash(body.data);
00189
00190
00191
00192
00193 string prefix = "";
00194 bool alreadyExists = false;
00195 if (index.Contains(chk)) {
00196 CacheIndexEntry currentEntry = index[chk];
00197 prefix = currentEntry.filename;
00198 if (File.Exists(BASE_PATH + currentEntry.filename + EXT)) {
00199 alreadyExists = true;
00200 } else {
00201 index.Remove(chk);
00202 }
00203 } else {
00204 prefix = Cache.StringFromChk(chk);
00205 if (File.Exists(BASE_PATH + prefix + EXT)) {
00206 alreadyExists = true;
00207 }
00208 }
00209 if (!alreadyExists) {
00210
00211 CacheEntry ce = new CacheEntry(body);
00212 byte[] buffer = ce.Serialise();
00213 if (buffer.Length + spaceUsed > maxSize) {
00214 Purge(maxSize - buffer.Length);
00215 }
00216
00217 CacheIndexEntry cie = new CacheIndexEntry(chk, prefix, buffer.Length, DateTime.Now);
00218
00219 FileStream newEntry = File.Create(BASE_PATH + prefix + EXT);
00220
00221 newEntry.Write(buffer, 0, buffer.Length);
00222
00223 newEntry.Close();
00224
00225 index.Add(cie);
00226 spaceUsed += cie.size;
00227 }
00228 }
00229 return chk;
00230 }
00231 }
00232
00238 public HTTPBody Get(byte[] chk) {
00239 lock (SyncRoot) {
00240 HTTPBody result;
00241 CacheIndexEntry indexEntry = index[chk];
00242 FileStream fs;
00243 try {
00244 fs = File.Open(BASE_PATH + indexEntry.filename + EXT, FileMode.Open, FileAccess.ReadWrite);
00245 } catch (Exception ex) {
00246 throw new CacheNotFoundException(String.Format("Unable to find {0}", Cache.StringFromChk(chk)), ex);
00247 }
00248
00249
00250
00251
00252
00253 CacheEntry cacheEntry = new CacheEntry(fs);
00254 result = cacheEntry.data;
00255
00256
00257
00258
00259 fs.Close();
00260
00261
00262 return result;
00263 }
00264 }
00265
00270 public void Delete(byte[] chk) {
00271 lock (SyncRoot) {
00272 CacheIndexEntry indexEntry = index[chk];
00273 if (File.Exists(BASE_PATH + indexEntry.filename + EXT)) {
00274 File.Delete(BASE_PATH + indexEntry.filename + EXT);
00275 }
00276 index.Remove(chk);
00277 spaceUsed -= indexEntry.size;
00278 }
00279 }
00280
00285 public void Purge(long targetUsage) {
00286 lock (SyncRoot) {
00287 long bytesRemaining = spaceUsed - targetUsage;
00288 while (bytesRemaining > 0) {
00289 byte[] evictionTargetCHK = index.LeastRecentlyUsed().chk;
00290 Delete(evictionTargetCHK);
00291 cacheManager.uriMapper.RemoveAssociatedUris(evictionTargetCHK);
00292 bytesRemaining = spaceUsed - targetUsage;
00293 }
00294 }
00295 }
00296
00297 public bool Contains(byte[] chk) {
00298 lock (SyncRoot) {
00299 return File.Exists(BASE_PATH + Cache.StringFromChk(chk) + EXT);
00300 }
00301 }
00306 public void Delete(string chk) {
00307 Delete(Cache.ChkFromString(chk));
00308 }
00309
00315 public static string StringFromChk(byte[] chk) {
00316 string result = "";
00317 foreach (byte b in chk) {
00318 result += getHex(b);
00319 }
00320 return result;
00321 }
00322
00328 public static string getHex(byte src) {
00329 return src.ToString("X2");
00330 }
00331
00337 public static byte[] ChkFromString(string src) {
00338 byte[] result = new byte[20];
00339 if (src.Length == 40) {
00340 string chunk;
00341 for (int i = 0; i < src.Length; i+=2) {
00342 chunk = src.Substring(i, 2);
00343 result[i/2] = Byte.Parse(chunk, System.Globalization.NumberStyles.HexNumber);
00344 }
00345 } else {
00346 throw new ApplicationException("Expected string of length 40. Got length " + src.Length);
00347 }
00348 return result;
00349 }
00350
00355 public CacheIndex GetCacheIndex() {
00356 return index;
00357 }
00358 }
00359
00360
00361 public class CacheEntry {
00362 public HTTPBody data;
00363 public DateTime lastUsed;
00364
00365 public CacheEntry(HTTPBody data) {
00366 this.data = data;
00367 this.lastUsed = DateTime.MinValue;
00368 }
00369
00370 public CacheEntry(FileStream fs) {
00371 byte[] buffer = new byte[fs.Length];
00372 fs.Read(buffer, 0, (int)fs.Length);
00373 Deserialise(buffer);
00374 }
00375
00376 public byte[] Serialise() {
00377 byte[] lastUsedBuffer = BitConverter.GetBytes(lastUsed.Ticks);
00378 byte[] buffer = new byte[lastUsedBuffer.Length + data.data.Length];
00379 Array.Copy(lastUsedBuffer, 0, buffer, 0, lastUsedBuffer.Length);
00380 Array.Copy(data.data, 0, buffer, lastUsedBuffer.Length, data.data.Length);
00381 return buffer;
00382 }
00383
00384 public void Deserialise(byte[] src) {
00385 int dateTimeLength = BitConverter.GetBytes(DateTime.Now.Ticks).Length;
00386 byte[] bodyBuffer = new byte[src.Length - dateTimeLength];
00387 Array.Copy(src, dateTimeLength, bodyBuffer, 0, src.Length - dateTimeLength);
00388
00389 this.data = new HTTPBody(bodyBuffer);
00390 this.lastUsed = new DateTime(BitConverter.ToInt64(src, 0));
00391 }
00392
00393 public static DateTime DeserialiseDate(FileStream fs) {
00394 byte[] dtBuffer = new byte[8];
00395 fs.Seek(0, SeekOrigin.Begin);
00396 fs.Read(dtBuffer, 0, dtBuffer.Length);
00397 return new DateTime(BitConverter.ToInt64(dtBuffer, 0));
00398 }
00399
00400 public byte[] SerialiseDate() {
00401 return BitConverter.GetBytes(lastUsed.Ticks);
00402 }
00403
00404 public static int SizeOfSerialised(HTTPBody data) {
00405 return (BitConverter.GetBytes(DateTime.Now.Ticks).Length + data.data.Length);
00406 }
00407
00408 }
00409
00410
00411
00412 public class URIMapper : Hashtable {
00413 public URIMapper() : base() {}
00414
00415 public void Add(Uri key, URIMappingEntry data) {
00416 lock (this) {
00417 base.Add(key, data);
00418 }
00419 }
00420
00421 public URIMappingEntry this[Uri key] {
00422 get { return (URIMappingEntry)base[key];}
00423 set { lock (this){
00424 base[key] = value;
00425 }
00426 }
00427 }
00428
00429 public void Remove(Uri key) {
00430 lock (this) {
00431 base.Remove(key);
00432 }
00433 }
00434
00439 public void RemoveAssociatedUris(byte[] chk) {
00440 ArrayList killList = new ArrayList();
00441 lock (this) {
00442 foreach (DictionaryEntry de in this) {
00443 if (Cache.StringFromChk(((URIMappingEntry)de.Value).CHK) == Cache.StringFromChk(chk)) {
00444 killList.Add(de.Key);
00445 }
00446 }
00447 foreach (object o in killList) {
00448 this.Remove((Uri)o);
00449 }
00450 }
00451 }
00452
00453 public bool IsFresh(Uri key) {
00454 return !this[key].HasExpired;
00455 }
00456
00457 public bool Contains(Uri key) {
00458 return base.Contains(key);
00459 }
00460 }
00461
00462 public class URIMappingEntry {
00463 Uri key;
00464 byte[] chk;
00465 DateTime creationStamp;
00466 string headers;
00467
00468 public URIMappingEntry(Uri key, byte[] chk, string headers) {
00469 this.key = key;
00470 this.chk = chk;
00471 this.headers = headers;
00472 creationStamp = DateTime.Now;
00473 }
00474
00475 public bool HasExpired {
00476 get { return (DateTime.Now - creationStamp) > TimeSpan.FromMinutes(15); }
00477 }
00478
00479 public byte[] CHK {
00480 get { return chk; }
00481 }
00482
00483 public string Headers {
00484 get { return headers; }
00485 }
00486
00487 public void Freshen(byte[] newChk, string newHeaders) {
00488 creationStamp = DateTime.Now;
00489 if (headers != newHeaders) {
00490 headers = newHeaders;
00491 }
00492 if (chk != null && newChk != null && Cache.StringFromChk(chk) != Cache.StringFromChk(newChk)) {
00493 chk = newChk;
00494 }
00495 }
00496 }
00497
00498 public class CacheNotFoundException : ApplicationException {
00499 public CacheNotFoundException(string message, Exception innerException) : base (message, innerException) {
00500 }
00501 public CacheNotFoundException(string message) : base (message) {
00502 }
00503 public CacheNotFoundException(): base() {
00504 }
00505 }
00506 }