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(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(); /// /// Saves the response content to disk and updates the metadata. /// async Task 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); } /// /// Writes metadata to disk and caches it in memory. /// 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; } }