From 2cb3c1c331273c356e6b5a71218bcce13cfcf192 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 5 Apr 2025 20:05:23 +0900 Subject: [PATCH] Add the cached blob --- .../WelsonJS.Launcher/ResourceServer.cs | 118 +++++++++++++++++- .../ResourceTools/Completion.cs | 4 +- .../WelsonJS.Launcher/ResourceTools/Config.cs | 4 +- .../ResourceTools/DnsQuery.cs | 4 +- .../WelsonJS.Launcher/ResourceTools/Tfa.cs | 4 +- 5 files changed, 123 insertions(+), 11 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 449abcb..02f3e0b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -5,6 +5,7 @@ using System.IO; using System.Net; using System.Net.Http; using System.Reflection; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,6 +24,7 @@ namespace WelsonJS.Launcher private List _tools = new List(); private const int _blobTimeout = 5000; private readonly HttpClient _blobClient = new HttpClient(); + private readonly string _defaultMimeType = "application/octet-stream"; public ResourceServer(string prefix, string resourceName) { @@ -128,8 +130,28 @@ namespace WelsonJS.Launcher private async Task ServeBlob(HttpListenerContext context, string path) { + byte[] data; + string mimeType; + + // Try serve data from the cached blob + if (TryGetCachedBlob(path, out mimeType, true)) + { + if (TryGetCachedBlob(path, out data)) + { + if (String.IsNullOrEmpty(mimeType)) + { + mimeType = _defaultMimeType; + } + + ServeResource(context, data, mimeType); + return true; + } + } + + // If not cached yet try { + string blobServerPrefix = Program.GetAppConfig("BlobServerPrefix"); string url = $"{blobServerPrefix}{path}"; @@ -143,16 +165,106 @@ namespace WelsonJS.Launcher return false; } - byte[] data = await response.Content.ReadAsByteArrayAsync(); - string mimeType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream"; + data = await response.Content.ReadAsByteArrayAsync(); + mimeType = response.Content.Headers.ContentType?.MediaType ?? _defaultMimeType; ServeResource(context, data, mimeType); + _ = TrySaveCachedBlob(path, data, mimeType); return true; } catch (Exception ex) { - Trace.TraceError($"Failed to serve blob. Exception: {ex.ToString()}"); + Trace.TraceError($"Failed to serve blob. Exception: {ex.Message}"); + return false; + } + } + + private string GetCachedBlobPath(string path) + { + // Get a hash from the path + string hashedPath; + using (MD5 md5 = MD5.Create()) + { + byte[] bHashedPath = md5.ComputeHash(Encoding.UTF8.GetBytes(path)); + hashedPath = BitConverter.ToString(bHashedPath).Replace("-", "").ToLowerInvariant(); + } + + // Get a sub-directory paths from the hashed path + string[] subDirectoryPaths = new string[] { + hashedPath.Substring(0, 2), + hashedPath.Substring(2, 2), + hashedPath.Substring(4, 2) + }; + + // Return the cache path + return Path.Combine(Program.GetAppDataPath(), "BlobCache", String.Join("\\", subDirectoryPaths), hashedPath); + } + + private bool TryGetCachedBlob(string path, out byte[] data, bool isMetadata = false) + { + string cachePath = GetCachedBlobPath(path); + if (isMetadata) + { + cachePath = $"{cachePath}.meta"; + } + + try + { + if (File.Exists(cachePath)) + { + data = File.ReadAllBytes(cachePath); + return true; + } + } + catch (Exception ex) + { + Trace.TraceError($"Error: {ex.Message}"); + } + + data = null; + return false; + } + + private bool TryGetCachedBlob(string path, out string data, bool isMetadata = false) + { + byte[] bData; + if (TryGetCachedBlob(path, out bData, isMetadata)) + { + data = Encoding.UTF8.GetString(bData); + return true; + } + + data = null; + return false; + } + + private async Task TrySaveCachedBlob(string path, byte[] data, string mimeType) + { + await Task.Delay(0); + + try + { + string cachePath = GetCachedBlobPath(path); + string cacheDirectory = Path.GetDirectoryName(cachePath); + + // Is exists the cached blob directory + if (!Directory.Exists(cacheDirectory)) + { + Directory.CreateDirectory(cacheDirectory); + } + + // Save the cache + File.WriteAllBytes(cachePath, data); + + // Save the cache meta + File.WriteAllBytes($"{cachePath}.meta", Encoding.UTF8.GetBytes(mimeType)); + + return true; + } + catch (Exception ex) + { + Trace.TraceError($"Error: {ex.Message}"); return false; } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs index e90fa37..d9d6d81 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs @@ -36,6 +36,8 @@ namespace WelsonJS.Launcher.ResourceTools public async Task HandleAsync(HttpListenerContext context, string path) { + await Task.Delay(0); + string word = path.Substring(Prefix.Length); try @@ -67,8 +69,6 @@ namespace WelsonJS.Launcher.ResourceTools { Server.ServeResource(context, $"Failed to process completion request. {ex.Message}", "application/xml", 500); } - - await Task.Delay(0); } private List GetInstalledSoftwareExecutables() diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Config.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Config.cs index e07dc84..6358766 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Config.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Config.cs @@ -21,6 +21,8 @@ namespace WelsonJS.Launcher.ResourceTools public async Task HandleAsync(HttpListenerContext context, string path) { + await Task.Delay(0); + string configName = path.Substring(Prefix.Length); try @@ -32,8 +34,6 @@ namespace WelsonJS.Launcher.ResourceTools { Server.ServeResource(context, $"Failed to process Config request. {ex.Message}", "application/xml", 500); } - - await Task.Delay(0); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs index c911791..fa4ca2a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs @@ -30,6 +30,8 @@ namespace WelsonJS.Launcher.ResourceTools public async Task HandleAsync(HttpListenerContext context, string path) { + await Task.Delay(0); + string query = path.Substring(Prefix.Length); if (string.IsNullOrWhiteSpace(query) || query.Length > 255) @@ -59,8 +61,6 @@ namespace WelsonJS.Launcher.ResourceTools { Server.ServeResource(context, $"Failed to process DNS query. {ex.Message}", "application/xml", 500); } - - await Task.Delay(0); } public List QueryA(string domain) => QueryDns(domain, 1); diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Tfa.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Tfa.cs index a6e68df..52de1d9 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Tfa.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Tfa.cs @@ -25,6 +25,8 @@ namespace WelsonJS.Launcher.ResourceTools public async Task HandleAsync(HttpListenerContext context, string path) { + await Task.Delay(0); + string endpoint = path.Substring(Prefix.Length); if (endpoint.Equals("pubkey")) @@ -34,8 +36,6 @@ namespace WelsonJS.Launcher.ResourceTools } Server.ServeResource(context); - - await Task.Delay(0); } public int GetOtp(string key)