From ee5bffde884354b3e5400769a2560f882ee648da Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Tue, 16 Dec 2025 01:57:02 +1000 Subject: [PATCH] Fix: Exclude active cache folders in GetCachedFrameAsync to prevent reading incomplete data - Add active cache folder exclusion to GetCachedFrameAsync - Prevents TaskCanceledException when reading metadata from folders being written to - Matches pattern used in GetCachedRadarAsync for consistency --- Program.cs | 74 ++++++++++++++++++++++++++++++++++++++++ Services/CacheService.cs | 5 ++- appsettings.json | 6 ++++ docker-compose.yml | 8 +++++ 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index aadec34..29d5b9c 100644 --- a/Program.cs +++ b/Program.cs @@ -9,6 +9,74 @@ builder.Services.AddControllersWithViews(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApi(); +// Configure CORS - MUST be added before other services +var corsOrigins = builder.Configuration.GetValue("Cors:AllowedOrigins", "*"); +var corsMethods = builder.Configuration.GetValue("Cors:AllowedMethods", "GET,POST,OPTIONS"); +var corsHeaders = builder.Configuration.GetValue("Cors:AllowedHeaders", "*"); +var corsAllowCredentials = builder.Configuration.GetValue("Cors:AllowCredentials", false); + +// Support environment variable override (comma-separated for multiple origins) +var corsOriginsEnv = Environment.GetEnvironmentVariable("CORS__ALLOWEDORIGINS"); +if (!string.IsNullOrEmpty(corsOriginsEnv)) +{ + corsOrigins = corsOriginsEnv; +} + +var corsMethodsEnv = Environment.GetEnvironmentVariable("CORS__ALLOWEDMETHODS"); +if (!string.IsNullOrEmpty(corsMethodsEnv)) +{ + corsMethods = corsMethodsEnv; +} + +var corsHeadersEnv = Environment.GetEnvironmentVariable("CORS__ALLOWEDHEADERS"); +if (!string.IsNullOrEmpty(corsHeadersEnv)) +{ + corsHeaders = corsHeadersEnv; +} + +var corsAllowCredentialsEnv = Environment.GetEnvironmentVariable("CORS__ALLOWCREDENTIALS"); +if (!string.IsNullOrEmpty(corsAllowCredentialsEnv) && bool.TryParse(corsAllowCredentialsEnv, out var parsedCredentials)) +{ + corsAllowCredentials = parsedCredentials; +} + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + if (corsOrigins == "*") + { + policy.AllowAnyOrigin(); + } + else + { + // Split comma-separated origins + var origins = corsOrigins.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + policy.WithOrigins(origins); + } + + // Split comma-separated methods + var methods = corsMethods.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + policy.WithMethods(methods); + + // Split comma-separated headers or allow all + if (corsHeaders == "*") + { + policy.AllowAnyHeader(); + } + else + { + var headers = corsHeaders.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + policy.WithHeaders(headers); + } + + if (corsAllowCredentials) + { + policy.AllowCredentials(); + } + }); +}); + // Register core services via interfaces (order matters - dependencies must be registered first) builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -45,6 +113,12 @@ if (enableHttpsRedirection) app.UseHttpsRedirection(); } +// Enable routing (required for CORS to work with controllers) +app.UseRouting(); + +// CORS middleware - MUST be after UseRouting but before MapControllers +app.UseCors(); + // No authorization required - service is designed to run behind a reverse proxy if auth is needed // Map MVC routes first (before API routes to avoid conflicts) diff --git a/Services/CacheService.cs b/Services/CacheService.cs index 9aa4236..f1611e1 100644 --- a/Services/CacheService.cs +++ b/Services/CacheService.cs @@ -116,7 +116,10 @@ public class CacheService : ICacheService return null; } - var (cacheFolderPath, _) = await GetCachedScreenshotWithMetadataAsync(suburb, state, CachedDataType.Radar, null, cancellationToken); + // Exclude active cache folder (currently being written to) to avoid reading incomplete data + var locationKey = LocationHelper.GetLocationKey(suburb, state); + var excludeFolder = GetActiveCacheFolder(locationKey); + var (cacheFolderPath, _) = await GetCachedScreenshotWithMetadataAsync(suburb, state, CachedDataType.Radar, excludeFolder, cancellationToken); if (string.IsNullOrEmpty(cacheFolderPath)) { diff --git a/appsettings.json b/appsettings.json index 4716789..39da934 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,6 +6,12 @@ } }, "AllowedHosts": "*", + "Cors": { + "AllowedOrigins": "*", + "AllowedMethods": "GET,POST,OPTIONS", + "AllowedHeaders": "*", + "AllowCredentials": false + }, "CacheDirectory": "./cache", "CacheRetentionHours": 24, "CacheExpirationMinutes": 12.5, diff --git a/docker-compose.yml b/docker-compose.yml index 073040c..a3355d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,14 @@ services: # Virtual display for non-headless browser - DISPLAY=:99 + # CORS Configuration (for Home Assistant integration) + # Multiple origins can be comma-separated: "http://homeassistant.local:8123,http://192.168.1.100:8123" + # Use "*" to allow all origins (less secure, but convenient for development) + - CORS__ALLOWEDORIGINS=${CORS__ALLOWEDORIGINS:-*} + - CORS__ALLOWEDMETHODS=${CORS__ALLOWEDMETHODS:-GET,POST,OPTIONS} + - CORS__ALLOWEDHEADERS=${CORS__ALLOWEDHEADERS:-*} + - CORS__ALLOWCREDENTIALS=${CORS__ALLOWCREDENTIALS:-false} + # Application configuration (override appsettings.json) # Use double underscore (__) for nested config keys - CACHEDIRECTORY=${CACHEDIRECTORY:-/app/cache}