diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs
index ca0759f..7ec8500 100644
--- a/Rest.Net.Example/Program.cs
+++ b/Rest.Net.Example/Program.cs
@@ -28,6 +28,8 @@ namespace MontoyaTech.Rest.Net.Example
}
}
+ public static RouteFileCache FileCache = new RouteFileCache(100 * 1024 * 1024);
+
public static void Main(string[] args)
{
File.WriteAllText("test.txt", "hello from a file");
@@ -98,7 +100,9 @@ namespace MontoyaTech.Rest.Net.Example
[RouteResponse(typeof(string))]
public static HttpListenerResponse CompressFile(HttpListenerContext context)
{
- return context.Response.WithStatus(HttpStatusCode.OK).WithCompressedFile("test.txt");
+ var content = FileCache.Cache("test.txt");
+
+ return context.Response.WithStatus(HttpStatusCode.OK).WithCompressedFile("test.txt", content);
}
[RouteGroup("Test")]
diff --git a/Rest.Net/HttpListenerResponseExtensions.cs b/Rest.Net/HttpListenerResponseExtensions.cs
index 0cd202d..7ecbdd2 100644
--- a/Rest.Net/HttpListenerResponseExtensions.cs
+++ b/Rest.Net/HttpListenerResponseExtensions.cs
@@ -114,6 +114,33 @@ namespace MontoyaTech.Rest.Net
return response;
}
+ ///
+ /// Sets the response content type to a file and writes the file content with name to the response.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static HttpListenerResponse WithFile(this HttpListenerResponse response, string fileName, byte[] content, string mimeType = null)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ throw new ArgumentException("fileName must not be null or empty");
+
+ if (string.IsNullOrWhiteSpace(mimeType))
+ mimeType = Path.GetExtension(fileName).GetMimeType();
+
+ response.ContentType = mimeType;
+ response.Headers.Add("Content-Deposition", $@"attachment; filename=""{Path.GetFileName(fileName)}""");
+ response.SendChunked = true;
+
+ using (var responseStream = response.OutputStream)
+ responseStream.Write(content, 0, content.Length);
+
+ return response;
+ }
+
///
/// Sets the response content type to a file and compresses the given file content to the response.
///
@@ -142,6 +169,35 @@ namespace MontoyaTech.Rest.Net
return response;
}
+ ///
+ /// Sets the response content type to a file and writes the file content with name to the response.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static HttpListenerResponse WithCompressedFile(this HttpListenerResponse response, string fileName, byte[] content, string mimeType = null)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ throw new ArgumentException("filePath must not be null or empty");
+
+ if (string.IsNullOrWhiteSpace(mimeType))
+ mimeType = Path.GetExtension(fileName).GetMimeType();
+
+ response.ContentType = mimeType;
+ response.Headers.Add("Content-Deposition", $@"attachment; filename=""{Path.GetFileName(fileName)}""");
+ response.Headers.Add("Content-Encoding", "gzip");
+ response.SendChunked = true;
+
+ using (var responseStream = response.OutputStream)
+ using (var compressedStream = new GZipStream(responseStream, CompressionMode.Compress, true))
+ compressedStream.Write(content, 0, content.Length);
+
+ return response;
+ }
+
///
/// Sets the response content type to html and writes the given html to it.
///
diff --git a/Rest.Net/Rest.Net.csproj b/Rest.Net/Rest.Net.csproj
index 83a6e69..50659b8 100644
--- a/Rest.Net/Rest.Net.csproj
+++ b/Rest.Net/Rest.Net.csproj
@@ -17,7 +17,7 @@
MontoyaTech.Rest.Net
MontoyaTech.Rest.Net
True
- 1.3.3
+ 1.3.4
Logo_Symbol_Black_Outline.png
diff --git a/Rest.Net/RouteFileCache.cs b/Rest.Net/RouteFileCache.cs
new file mode 100644
index 0000000..61e9bc2
--- /dev/null
+++ b/Rest.Net/RouteFileCache.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+using System.Collections.Concurrent;
+using System.Security.Principal;
+
+namespace MontoyaTech.Rest.Net
+{
+ ///
+ /// The outline of a FileCache that can be used with Routes to speed
+ /// up response times.
+ ///
+ public class RouteFileCache
+ {
+ ///
+ /// The outline of a CachedFile.
+ ///
+ public class CachedFile
+ {
+ public string FilePath;
+
+ public byte[] Content;
+
+ public int HitCount;
+
+ public DateTime Cached = DateTime.Now;
+
+ public DateTime LastAccess = DateTime.Now;
+
+ public CachedFile(string filePath, byte[] content)
+ {
+ this.FilePath = filePath;
+ this.Content = content;
+ }
+ }
+
+ ///
+ /// The files that are currently cached.
+ ///
+ public Dictionary CachedFiles = new Dictionary();
+
+ ///
+ /// The current number of bytes cached.
+ ///
+ public int BytesCached { get; internal set; }
+
+ ///
+ /// The max bytes that can be cached at a time.
+ ///
+ public int MaxBytesCached { get; internal set; }
+
+ ///
+ /// Creates a new RouteFileCache with the max bytes that can be cached.
+ ///
+ ///
+ public RouteFileCache(int maxBytes)
+ {
+ this.MaxBytesCached = maxBytes;
+ }
+
+ ///
+ /// Returns whether or not a file was cached and the content of the file if it was.
+ ///
+ ///
+ ///
+ ///
+ public bool Cached(string filePath, out byte[] content)
+ {
+ lock (this.CachedFiles)
+ {
+ content = null;
+
+ if (!this.CachedFiles.ContainsKey(filePath))
+ return false;
+
+ var cached = this.CachedFiles[filePath];
+ cached.HitCount++;
+ cached.LastAccess = DateTime.Now;
+
+ content = cached.Content;
+
+ return true;
+ }
+ }
+
+ ///
+ /// Caches a new file and returns the content, if the file was already cached the content will be returned.
+ ///
+ ///
+ ///
+ ///
+ public byte[] Cache(string filePath)
+ {
+ lock (this.CachedFiles)
+ {
+ if (!this.CachedFiles.ContainsKey(filePath))
+ {
+ if (File.Exists(filePath))
+ {
+ var content = File.ReadAllBytes(filePath);
+
+ this.Cache(filePath, content);
+
+ return content;
+ }
+ else
+ {
+ throw new FileNotFoundException("Failed to find file: " + filePath);
+ }
+ }
+ else
+ {
+ var cached = this.CachedFiles[filePath];
+
+ cached.HitCount++;
+ cached.LastAccess = DateTime.Now;
+
+ return cached.Content;
+ }
+ }
+ }
+
+ ///
+ /// Caches a new file with the content, if the file was already cached this will just count as an access and overwrite the content.
+ ///
+ ///
+ ///
+ public void Cache(string filePath, byte[] content)
+ {
+ lock (this.CachedFiles)
+ {
+ if (!this.CachedFiles.ContainsKey(filePath))
+ {
+ if ((this.BytesCached + content.Length) > this.MaxBytesCached)
+ this.Free((this.BytesCached + content.Length) - this.MaxBytesCached);
+
+ var cached = new CachedFile(filePath, content);
+
+ this.BytesCached += content.Length;
+
+ this.CachedFiles.Add(filePath, cached);
+ }
+ else
+ {
+ var cached = this.CachedFiles[filePath];
+
+ cached.HitCount++;
+ cached.LastAccess = DateTime.Now;
+ cached.Content = content;
+ }
+ }
+ }
+
+ ///
+ /// Attempts to free up space in the cache by a specific amount of bytes.
+ ///
+ ///
+ public void Free(int neededBytes)
+ {
+ while (this.BytesCached + neededBytes >= this.MaxBytesCached)
+ {
+ CachedFile weakest = null;
+
+ foreach (var pair in this.CachedFiles)
+ {
+ if (weakest == null)
+ weakest = pair.Value;
+ else if (pair.Value.LastAccess < weakest.LastAccess && pair.Value.HitCount < weakest.HitCount)
+ weakest = pair.Value;
+ }
+
+ if (weakest != null)
+ {
+ this.CachedFiles.Remove(weakest.FilePath);
+
+ this.BytesCached -= weakest.Content.Length;
+ }
+ }
+ }
+ }
+}