Merge pull request #221 from gnh1201/dev

Add the cached blob #220
This commit is contained in:
Namhyeon Go 2025-04-05 20:13:40 +09:00 committed by GitHub
commit 46d0244c34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 123 additions and 11 deletions

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -23,6 +24,7 @@ namespace WelsonJS.Launcher
private List<IResourceTool> _tools = new List<IResourceTool>(); private List<IResourceTool> _tools = new List<IResourceTool>();
private const int _blobTimeout = 5000; private const int _blobTimeout = 5000;
private readonly HttpClient _blobClient = new HttpClient(); private readonly HttpClient _blobClient = new HttpClient();
private readonly string _defaultMimeType = "application/octet-stream";
public ResourceServer(string prefix, string resourceName) public ResourceServer(string prefix, string resourceName)
{ {
@ -128,8 +130,28 @@ namespace WelsonJS.Launcher
private async Task<bool> ServeBlob(HttpListenerContext context, string path) private async Task<bool> 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 try
{ {
string blobServerPrefix = Program.GetAppConfig("BlobServerPrefix"); string blobServerPrefix = Program.GetAppConfig("BlobServerPrefix");
string url = $"{blobServerPrefix}{path}"; string url = $"{blobServerPrefix}{path}";
@ -143,16 +165,106 @@ namespace WelsonJS.Launcher
return false; return false;
} }
byte[] data = await response.Content.ReadAsByteArrayAsync(); data = await response.Content.ReadAsByteArrayAsync();
string mimeType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream"; mimeType = response.Content.Headers.ContentType?.MediaType ?? _defaultMimeType;
ServeResource(context, data, mimeType); ServeResource(context, data, mimeType);
_ = TrySaveCachedBlob(path, data, mimeType);
return true; return true;
} }
catch (Exception ex) 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<bool> 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; return false;
} }
} }

View File

@ -36,6 +36,8 @@ namespace WelsonJS.Launcher.ResourceTools
public async Task HandleAsync(HttpListenerContext context, string path) public async Task HandleAsync(HttpListenerContext context, string path)
{ {
await Task.Delay(0);
string word = path.Substring(Prefix.Length); string word = path.Substring(Prefix.Length);
try try
@ -67,8 +69,6 @@ namespace WelsonJS.Launcher.ResourceTools
{ {
Server.ServeResource(context, $"<error>Failed to process completion request. {ex.Message}</error>", "application/xml", 500); Server.ServeResource(context, $"<error>Failed to process completion request. {ex.Message}</error>", "application/xml", 500);
} }
await Task.Delay(0);
} }
private List<string> GetInstalledSoftwareExecutables() private List<string> GetInstalledSoftwareExecutables()

View File

@ -21,6 +21,8 @@ namespace WelsonJS.Launcher.ResourceTools
public async Task HandleAsync(HttpListenerContext context, string path) public async Task HandleAsync(HttpListenerContext context, string path)
{ {
await Task.Delay(0);
string configName = path.Substring(Prefix.Length); string configName = path.Substring(Prefix.Length);
try try
@ -32,8 +34,6 @@ namespace WelsonJS.Launcher.ResourceTools
{ {
Server.ServeResource(context, $"<error>Failed to process Config request. {ex.Message}</error>", "application/xml", 500); Server.ServeResource(context, $"<error>Failed to process Config request. {ex.Message}</error>", "application/xml", 500);
} }
await Task.Delay(0);
} }
} }
} }

View File

@ -30,6 +30,8 @@ namespace WelsonJS.Launcher.ResourceTools
public async Task HandleAsync(HttpListenerContext context, string path) public async Task HandleAsync(HttpListenerContext context, string path)
{ {
await Task.Delay(0);
string query = path.Substring(Prefix.Length); string query = path.Substring(Prefix.Length);
if (string.IsNullOrWhiteSpace(query) || query.Length > 255) if (string.IsNullOrWhiteSpace(query) || query.Length > 255)
@ -59,8 +61,6 @@ namespace WelsonJS.Launcher.ResourceTools
{ {
Server.ServeResource(context, $"<error>Failed to process DNS query. {ex.Message}</error>", "application/xml", 500); Server.ServeResource(context, $"<error>Failed to process DNS query. {ex.Message}</error>", "application/xml", 500);
} }
await Task.Delay(0);
} }
public List<string> QueryA(string domain) => QueryDns(domain, 1); public List<string> QueryA(string domain) => QueryDns(domain, 1);

View File

@ -25,6 +25,8 @@ namespace WelsonJS.Launcher.ResourceTools
public async Task HandleAsync(HttpListenerContext context, string path) public async Task HandleAsync(HttpListenerContext context, string path)
{ {
await Task.Delay(0);
string endpoint = path.Substring(Prefix.Length); string endpoint = path.Substring(Prefix.Length);
if (endpoint.Equals("pubkey")) if (endpoint.Equals("pubkey"))
@ -34,8 +36,6 @@ namespace WelsonJS.Launcher.ResourceTools
} }
Server.ServeResource(context); Server.ServeResource(context);
await Task.Delay(0);
} }
public int GetOtp(string key) public int GetOtp(string key)