assets server "partial" mode improvements

* add support for cache/store assets in partial mode
* use stream approach in assets proxy mode
	* reduce memory usage on proxy big files
	* reduce risk of game client timeouts on slow connection
This commit is contained in:
Robert Paciorek 2024-01-12 17:52:50 +00:00
parent 7888445eee
commit bd6df27c92
2 changed files with 53 additions and 13 deletions

View File

@ -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 {

View File

@ -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;
}
}