From 2b892dfd66cd57b997dc0ed7d48c1334a14781d6 Mon Sep 17 00:00:00 2001 From: MattMo Date: Wed, 1 Mar 2023 16:19:20 -0800 Subject: [PATCH] Added a new RouteFileCache and more response extensions. Bumped package version to 1.3.4. --- Rest.Net.Example/Program.cs | 6 +- Rest.Net/HttpListenerResponseExtensions.cs | 56 +++++++ Rest.Net/Rest.Net.csproj | 2 +- Rest.Net/RouteFileCache.cs | 185 +++++++++++++++++++++ 4 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 Rest.Net/RouteFileCache.cs 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; + } + } + } + } +}