Files
PullForwardCache/Terribledev.PullForwardCache/Program.cs
Tommy Parnell 2bb8dee374 inti
2025-02-18 12:33:51 -05:00

123 lines
4.0 KiB
C#

using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
var app = builder.Build();
// Configure the HTTP request pipeline.
string cachePath = Environment.GetEnvironmentVariable("CACHE_PATH") ?? "cache";
if (!Directory.Exists(cachePath))
{
Directory.CreateDirectory(cachePath);
}
app.MapGet("/{url}", async (string url, IHttpClientFactory clientFactory, IMemoryCache memoryCache) =>
{
var client = clientFactory.CreateClient();
string encodedUrl = WebUtility.UrlEncode(url);
string filePath = Path.Combine(cachePath, encodedUrl);
string metadataPath = Path.Combine(cachePath, $"{encodedUrl}.json");
if (memoryCache.TryGetValue(encodedUrl, out FileMetadata? mdFromCache) && DateTime.Now <= mdFromCache?.Expiry)
{
return Results.File(filePath, mdFromCache.ContentType ?? "application/octet-stream");
}
if (File.Exists(filePath) && File.Exists(metadataPath))
{
var metadata = JsonSerializer.Deserialize<FileMetadata>(await File.ReadAllTextAsync(metadataPath)) ?? new FileMetadata();
var request = new HttpRequestMessage(HttpMethod.Get, url);
if (!string.IsNullOrEmpty(metadata.ETag))
{
request.Headers.IfNoneMatch.Add(EntityTagHeaderValue.Parse(metadata.ETag));
}
var response = await client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.NotModified)
{
metadata.CachedAt = DateTime.Now;
metadata.Expiry = DateTime.Now.AddHours(24);
await WriteMetadataAsync(metadataPath, metadata, memoryCache, encodedUrl);
return Results.File(filePath, metadata.ContentType ?? "application/octet-stream");
}
else if (response.IsSuccessStatusCode)
{
return await SaveAndReturnFile(response, filePath, metadataPath, memoryCache, encodedUrl, url);
}
}
try
{
var response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return Results.StatusCode((int)response.StatusCode);
}
return await SaveAndReturnFile(response, filePath, metadataPath, memoryCache, encodedUrl, url);
}
catch (Exception ex)
{
return Results.Problem(ex.Message, statusCode: 500);
}
});
app.Run();
/// <summary>
/// Saves the response content to disk and updates the metadata.
/// </summary>
async Task<IResult> SaveAndReturnFile(HttpResponseMessage response, string filePath, string metadataPath, IMemoryCache memoryCache, string encodedUrl, string url)
{
var content = await response.Content.ReadAsByteArrayAsync();
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
var eTag = response.Headers.ETag?.ToString();
await File.WriteAllBytesAsync(filePath, content);
var metadata = new FileMetadata
{
Url = url,
ContentType = contentType,
CachedAt = DateTime.Now,
Expiry = DateTime.Now.AddHours(24),
ETag = eTag
};
await WriteMetadataAsync(metadataPath, metadata, memoryCache, encodedUrl);
return Results.File(filePath, contentType);
}
/// <summary>
/// Writes metadata to disk and caches it in memory.
/// </summary>
async Task WriteMetadataAsync(string metadataPath, FileMetadata metadata, IMemoryCache memoryCache, string cacheKey)
{
await File.WriteAllTextAsync(metadataPath, JsonSerializer.Serialize(metadata));
memoryCache.Set(cacheKey, metadata, TimeSpan.FromMinutes(15));
}
public class FileMetadata
{
public string Url { get; set; } = string.Empty;
public string? ContentType { get; set; }
public DateTime CachedAt { get; set; }
public DateTime Expiry { get; set; }
public string? ETag { get; set; }
}