diff --git a/src/Configuration/AssetServerConfig.cs b/src/Configuration/AssetServerConfig.cs index fd63c5c..797039f 100644 --- a/src/Configuration/AssetServerConfig.cs +++ b/src/Configuration/AssetServerConfig.cs @@ -6,6 +6,7 @@ public class AssetServerConfig { public AssetServerMode Mode { get; set; } public string ProviderURL { get; set; } = string.Empty; public bool SubstituteMissingLocalAssets { get; set; } = false; + public bool UseCache { get; set; } = true; } public enum AssetServerMode { diff --git a/src/Middleware/AssetMiddleware.cs b/src/Middleware/AssetMiddleware.cs index 3546950..a829d7c 100644 --- a/src/Middleware/AssetMiddleware.cs +++ b/src/Middleware/AssetMiddleware.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Options; +using System.Net; +using System.IO; using sodoff.Configuration; namespace sodoff.Middleware; @@ -34,6 +36,9 @@ public class AssetMiddleware string localPath = GetLocalPath("assets/" + assetPath); + if (localPath == string.Empty && config.Value.Mode == AssetServerMode.Partial && config.Value.UseCache) + localPath = GetLocalPath("assets-cache/" + assetPath); + if (localPath == string.Empty) { if (config.Value.Mode == AssetServerMode.Partial) await GetRemoteAsset(context, assetPath); @@ -49,24 +54,58 @@ public class AssetMiddleware private async Task GetRemoteAsset(HttpContext context, string path) { HttpClient client = new HttpClient(); + string filePath = Path.GetFullPath("assets-cache/" + path); + string filePathTmp = filePath + Path.GetRandomFileName().Substring(0, 8); try { - var response = await client.GetAsync(config.Value.ProviderURL + path); - string? contentType = response.Content.Headers.ContentType?.MediaType; + using (var response = await client.GetAsync( + config.Value.ProviderURL + path, + HttpCompletionOption.ResponseHeadersRead + )) { + if (response.IsSuccessStatusCode) { + if (response.Content.Headers.ContentType?.MediaType != null) + context.Response.Headers["Content-Type"] = response.Content.Headers.ContentType?.MediaType; - if (contentType is null) { - context.Response.StatusCode = 404; - } - else if (contentType.StartsWith("application/octet-stream") || contentType.StartsWith("image/jpeg")) { - context.Response.Headers["Content-Type"] = contentType; - await response.Content.CopyToAsync(context.Response.Body); - } - else { - context.Response.ContentType = contentType; - await context.Response.WriteAsync(await response.Content.ReadAsStringAsync()); + if (response.Content.Headers.ContentLength != null) + context.Response.Headers["Content-Length"] = response.Content.Headers.ContentLength.ToString(); + + using (var inputStream = await response.Content.ReadAsStreamAsync()) { + if (config.Value.UseCache) { + string dirPath = Path.GetDirectoryName(filePath); + if (!Directory.Exists(dirPath)) { + Directory.CreateDirectory(dirPath); + } + + // copy data retrieved from upstream server to file and to response for game client + using (var fileStream = File.Open(filePathTmp, FileMode.Create)) { + // read response from upstream server + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { + // write to temporary file + var task1 = fileStream.WriteAsync(buffer, 0, bytesRead); + // send to client + var task2 = context.Response.Body.WriteAsync(buffer, 0, bytesRead); + // wait for finish both writes + await Task.WhenAll(task1, task2); + } + } + + // after successfully write data to temporary file, rename it to proper asset filename + File.Move(filePathTmp, filePath); + } else { + await inputStream.CopyToAsync(context.Response.Body); + } + } + } else { + context.Response.StatusCode = 404; + } } } catch (Exception) { - context.Response.StatusCode = 404; + if (File.Exists(filePathTmp)) + File.Delete(filePathTmp); + if (!context.Response.HasStarted) + context.Response.StatusCode = 502; } }