mirror of
https://github.com/alexhopeoconnor/bom-local-service.git
synced 2026-10-04 03:28:11 +10:00
Refactor cache validation and add startup cleanup
- Move cache folder completeness check to CacheHelper utility - Replace Width with RightOffset in screenshot crop config - Add startup cleanup to remove incomplete cache folders - Validate cache folders before use in BomRadarService
This commit is contained in:
@@ -19,10 +19,11 @@ public class ScreenshotCropConfig
|
|||||||
public int Y { get; set; } = 0;
|
public int Y { get; set; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Width of the crop area in pixels.
|
/// Right offset in pixels from the right edge of the map container.
|
||||||
/// If null, uses the remaining container width from X offset to the right edge.
|
/// How much to cut off from the right side.
|
||||||
|
/// Default: 0 (no cut from right edge).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Width { get; set; }
|
public int RightOffset { get; set; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Height of the crop area in pixels.
|
/// Height of the crop area in pixels.
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ if (enableHttpsRedirection)
|
|||||||
// No authorization required - service is designed to run behind a reverse proxy if auth is needed
|
// No authorization required - service is designed to run behind a reverse proxy if auth is needed
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Cleanup incomplete cache folders from previous crashes/restarts before starting services
|
||||||
|
var cacheService = app.Services.GetRequiredService<ICacheService>();
|
||||||
|
var deletedCount = cacheService.CleanupIncompleteCacheFolders();
|
||||||
|
if (deletedCount > 0)
|
||||||
|
{
|
||||||
|
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||||
|
logger.LogInformation("Startup recovery: cleaned up {Count} incomplete cache folder(s) from previous session", deletedCount);
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup on shutdown
|
// Cleanup on shutdown
|
||||||
app.Lifetime.ApplicationStopped.Register(() =>
|
app.Lifetime.ApplicationStopped.Register(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ public class BomRadarService : IBomRadarService, IDisposable
|
|||||||
// If there's an active update in progress, return existing cache (if available) with IsUpdating=true
|
// If there's an active update in progress, return existing cache (if available) with IsUpdating=true
|
||||||
if (isUpdating)
|
if (isUpdating)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(cacheFolderPath) && Directory.Exists(cacheFolderPath))
|
if (!string.IsNullOrEmpty(cacheFolderPath) && Directory.Exists(cacheFolderPath) && CacheHelper.IsCacheFolderComplete(cacheFolderPath))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Cache update in progress for {Suburb}, {State}, returning existing cached data", suburb, state);
|
_logger.LogInformation("Cache update in progress for {Suburb}, {State}, returning existing cached data", suburb, state);
|
||||||
var frames = await _cacheService.GetCachedFramesAsync(suburb, state, cancellationToken);
|
var frames = await _cacheService.GetCachedFramesAsync(suburb, state, cancellationToken);
|
||||||
@@ -181,7 +181,7 @@ public class BomRadarService : IBomRadarService, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(cacheFolderPath) && Directory.Exists(cacheFolderPath) && cachedMetadata != null)
|
if (!string.IsNullOrEmpty(cacheFolderPath) && Directory.Exists(cacheFolderPath) && cachedMetadata != null && CacheHelper.IsCacheFolderComplete(cacheFolderPath))
|
||||||
{
|
{
|
||||||
var isValid = _cacheService.IsCacheValid(cachedMetadata);
|
var isValid = _cacheService.IsCacheValid(cachedMetadata);
|
||||||
if (isValid)
|
if (isValid)
|
||||||
@@ -308,6 +308,26 @@ public class BomRadarService : IBomRadarService, IDisposable
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
// Always remove from active tracking, even on error
|
||||||
|
_activeCacheFolders.TryRemove(locationKey, out _);
|
||||||
|
|
||||||
|
// Clean up incomplete cache folder if error occurred
|
||||||
|
if (!string.IsNullOrEmpty(newCacheFolderPath) && Directory.Exists(newCacheFolderPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!CacheHelper.IsCacheFolderComplete(newCacheFolderPath))
|
||||||
|
{
|
||||||
|
Directory.Delete(newCacheFolderPath, recursive: true);
|
||||||
|
_logger.LogWarning("Cleaned up incomplete cache folder due to error: {Folder}", newCacheFolderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to clean up incomplete cache folder: {Folder}", newCacheFolderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up empty debug folder if we returned early without scraping
|
// Clean up empty debug folder if we returned early without scraping
|
||||||
if (!string.IsNullOrEmpty(debugFolder) && Directory.Exists(debugFolder))
|
if (!string.IsNullOrEmpty(debugFolder) && Directory.Exists(debugFolder))
|
||||||
{
|
{
|
||||||
|
|||||||
+56
-21
@@ -61,32 +61,13 @@ public class CacheService : ICacheService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if folder is complete: must have all 7 frames and metadata.json
|
// Check if folder is complete: must have all 7 frames, metadata.json, and frames.json
|
||||||
var hasAllFrames = true;
|
if (!CacheHelper.IsCacheFolderComplete(folder))
|
||||||
for (int i = 0; i < 7; i++)
|
|
||||||
{
|
|
||||||
var framePath = FilePathHelper.GetFrameFilePath(folder, i);
|
|
||||||
if (!File.Exists(framePath))
|
|
||||||
{
|
|
||||||
hasAllFrames = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasAllFrames)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Skipping incomplete cache folder: {Folder}", folder);
|
_logger.LogDebug("Skipping incomplete cache folder: {Folder}", folder);
|
||||||
continue; // Skip incomplete folders (currently being written to)
|
continue; // Skip incomplete folders (currently being written to)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if metadata exists (indicates folder is complete)
|
|
||||||
var metadataPath = FilePathHelper.GetMetadataFilePath(folder);
|
|
||||||
if (!File.Exists(metadataPath))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Skipping cache folder without metadata: {Folder}", folder);
|
|
||||||
continue; // Skip folders without metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// This folder is complete
|
// This folder is complete
|
||||||
var metadata = await LoadMetadataAsync(folder, cancellationToken);
|
var metadata = await LoadMetadataAsync(folder, cancellationToken);
|
||||||
return (folder, metadata);
|
return (folder, metadata);
|
||||||
@@ -347,5 +328,59 @@ public class CacheService : ICacheService
|
|||||||
/// Gets the cache directory path
|
/// Gets the cache directory path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetCacheDirectory() => _cacheDirectory;
|
public string GetCacheDirectory() => _cacheDirectory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up incomplete cache folders from previous crashes or restarts
|
||||||
|
/// </summary>
|
||||||
|
public int CleanupIncompleteCacheFolders()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_cacheDirectory))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deletedCount = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get all cache folders (they match the pattern LocationKey_Timestamp)
|
||||||
|
var folders = Directory.GetDirectories(_cacheDirectory)
|
||||||
|
.Where(f => !Path.GetFileName(f).StartsWith("debug", StringComparison.OrdinalIgnoreCase)) // Exclude debug folder
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var folder in folders)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check if folder is incomplete
|
||||||
|
if (!CacheHelper.IsCacheFolderComplete(folder))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Found incomplete cache folder from previous session, deleting: {Folder}", Path.GetFileName(folder));
|
||||||
|
Directory.Delete(folder, recursive: true);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to check or delete incomplete cache folder: {Folder}", folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedCount > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Startup cleanup completed: deleted {Count} incomplete cache folder(s)", deletedCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Startup cleanup: no incomplete cache folders found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during startup cleanup of incomplete cache folders");
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,5 +99,13 @@ public interface ICacheService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The full path to the cache directory</returns>
|
/// <returns>The full path to the cache directory</returns>
|
||||||
string GetCacheDirectory();
|
string GetCacheDirectory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up incomplete cache folders (e.g., from a previous crash or restart).
|
||||||
|
/// Scans all cache folders and deletes any that are incomplete (missing frames, metadata, etc.).
|
||||||
|
/// This should be called on application startup to recover from interrupted cache updates.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The number of incomplete folders that were deleted</returns>
|
||||||
|
int CleanupIncompleteCacheFolders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ public class ScrapingService : IScrapingService
|
|||||||
{
|
{
|
||||||
X = cropSection.GetValue<int>("X", 0),
|
X = cropSection.GetValue<int>("X", 0),
|
||||||
Y = cropSection.GetValue<int>("Y", 0),
|
Y = cropSection.GetValue<int>("Y", 0),
|
||||||
Width = cropSection.GetValue<int?>("Width"),
|
RightOffset = cropSection.GetValue<int>("RightOffset", 0),
|
||||||
Height = cropSection.GetValue<int?>("Height")
|
Height = cropSection.GetValue<int?>("Height")
|
||||||
};
|
};
|
||||||
|
|
||||||
_logger.LogInformation("Screenshot crop config: X={X}, Y={Y}, Width={Width}, Height={Height}",
|
_logger.LogInformation("Screenshot crop config: X={X}, Y={Y}, RightOffset={RightOffset}, Height={Height}",
|
||||||
_cropConfig.X, _cropConfig.Y, _cropConfig.Width, _cropConfig.Height);
|
_cropConfig.X, _cropConfig.Y, _cropConfig.RightOffset, _cropConfig.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -561,11 +561,11 @@ public class ScrapingService : IScrapingService
|
|||||||
var x = containerClip.X + _cropConfig.X;
|
var x = containerClip.X + _cropConfig.X;
|
||||||
var y = containerClip.Y + _cropConfig.Y;
|
var y = containerClip.Y + _cropConfig.Y;
|
||||||
|
|
||||||
// Calculate width (use configured or remaining width from X to right edge)
|
// Calculate width: container width minus left offset (X) minus right offset
|
||||||
var width = _cropConfig.Width ?? (containerClip.Width - _cropConfig.X);
|
var width = Math.Max(0, containerClip.Width - _cropConfig.X - _cropConfig.RightOffset);
|
||||||
|
|
||||||
// Calculate height (use configured or remaining height)
|
// Calculate height (use configured or remaining height)
|
||||||
var height = _cropConfig.Height ?? (containerClip.Height - _cropConfig.Y);
|
var height = _cropConfig.Height ?? Math.Max(0, containerClip.Height - _cropConfig.Y);
|
||||||
|
|
||||||
// Validate bounds
|
// Validate bounds
|
||||||
if (x < containerClip.X || y < containerClip.Y)
|
if (x < containerClip.X || y < containerClip.Y)
|
||||||
@@ -695,7 +695,36 @@ public class ScrapingService : IScrapingService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CaptureMapScreenshotAsync(IPage page, ILocator mapContainer, string outputPath, Clip containerClip)
|
private async Task CaptureMapScreenshotAsync(IPage page, ILocator mapContainer, string outputPath, Clip containerClip)
|
||||||
{
|
{
|
||||||
var cropArea = CalculateCropArea(containerClip);
|
Clip cropArea;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cropArea = CalculateCropArea(containerClip);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to calculate crop area, using full container bounds. Container: X={X}, Y={Y}, Width={Width}, Height={Height}",
|
||||||
|
containerClip.X, containerClip.Y, containerClip.Width, containerClip.Height);
|
||||||
|
// Fallback to full container if crop calculation fails
|
||||||
|
cropArea = containerClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate crop area is within page bounds before attempting screenshot
|
||||||
|
var viewportSize = page.ViewportSize;
|
||||||
|
if (viewportSize == null || cropArea.X < 0 || cropArea.Y < 0 ||
|
||||||
|
cropArea.X + cropArea.Width > viewportSize.Width ||
|
||||||
|
cropArea.Y + cropArea.Height > viewportSize.Height)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Crop area is outside viewport bounds, using full container. Crop: X={X}, Y={Y}, Width={Width}, Height={Height}, Viewport: {ViewportWidth}x{ViewportHeight}",
|
||||||
|
cropArea.X, cropArea.Y, cropArea.Width, cropArea.Height, viewportSize?.Width ?? 0, viewportSize?.Height ?? 0);
|
||||||
|
cropArea = containerClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final validation - ensure dimensions are positive
|
||||||
|
if (cropArea.Width <= 0 || cropArea.Height <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogError("Invalid crop dimensions: {Width}x{Height}, using full container", cropArea.Width, cropArea.Height);
|
||||||
|
cropArea = containerClip;
|
||||||
|
}
|
||||||
|
|
||||||
await page.ScreenshotAsync(new PageScreenshotOptions
|
await page.ScreenshotAsync(new PageScreenshotOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
namespace BomLocalService.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility methods for cache folder validation and operations.
|
||||||
|
/// </summary>
|
||||||
|
public static class CacheHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a cache folder is complete (has all required files).
|
||||||
|
/// A complete cache folder must have:
|
||||||
|
/// - All 7 frame images (frame_0.png through frame_6.png)
|
||||||
|
/// - metadata.json file
|
||||||
|
/// - frames.json file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cacheFolderPath">The path to the cache folder to check</param>
|
||||||
|
/// <returns>True if the folder is complete, false otherwise</returns>
|
||||||
|
public static bool IsCacheFolderComplete(string cacheFolderPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cacheFolderPath) || !Directory.Exists(cacheFolderPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for all 7 frames
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
var framePath = FilePathHelper.GetFrameFilePath(cacheFolderPath, i);
|
||||||
|
if (!File.Exists(framePath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for metadata.json
|
||||||
|
var metadataPath = FilePathHelper.GetMetadataFilePath(cacheFolderPath);
|
||||||
|
if (!File.Exists(metadataPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for frames.json
|
||||||
|
var framesMetadataPath = FilePathHelper.GetFramesMetadataFilePath(cacheFolderPath);
|
||||||
|
if (!File.Exists(framesMetadataPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -88,5 +88,14 @@ public static class FilePathHelper
|
|||||||
{
|
{
|
||||||
return Path.Combine(cacheFolderPath, ".writing");
|
return Path.Combine(cacheFolderPath, ".writing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the frames metadata file path within a cache folder.
|
||||||
|
/// Format: "{CacheFolderPath}/frames.json"
|
||||||
|
/// </summary>
|
||||||
|
public static string GetFramesMetadataFilePath(string cacheFolderPath)
|
||||||
|
{
|
||||||
|
return Path.Combine(cacheFolderPath, "frames.json");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
"Crop": {
|
"Crop": {
|
||||||
"X": 250,
|
"X": 250,
|
||||||
"Y": 0,
|
"Y": 0,
|
||||||
"Width": null,
|
"RightOffset": 250,
|
||||||
"Height": null
|
"Height": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user