From 375b9c3f14620583abd8506731037ccd32b75033 Mon Sep 17 00:00:00 2001 From: Tim Moore Date: Mon, 23 Feb 2026 17:00:54 +0000 Subject: [PATCH 1/5] Update package dependencies --- src/ImageMapper.Api/ImageMapper.Api.csproj | 6 +++--- src/ImageMapper.AppHost/ImageMapper.AppHost.csproj | 2 +- .../ImageMapper.ServiceDefaults.csproj | 14 +++++++------- src/ImageMapper.Tests/ImageMapper.Tests.csproj | 9 ++++++--- src/ImageMapper.Web/ImageMapper.Web.csproj | 4 ++-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ImageMapper.Api/ImageMapper.Api.csproj b/src/ImageMapper.Api/ImageMapper.Api.csproj index 79ba4b7..22a40d4 100644 --- a/src/ImageMapper.Api/ImageMapper.Api.csproj +++ b/src/ImageMapper.Api/ImageMapper.Api.csproj @@ -9,10 +9,10 @@ - - + + - + diff --git a/src/ImageMapper.AppHost/ImageMapper.AppHost.csproj b/src/ImageMapper.AppHost/ImageMapper.AppHost.csproj index 561b4b6..bdd18d7 100644 --- a/src/ImageMapper.AppHost/ImageMapper.AppHost.csproj +++ b/src/ImageMapper.AppHost/ImageMapper.AppHost.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/ImageMapper.ServiceDefaults/ImageMapper.ServiceDefaults.csproj b/src/ImageMapper.ServiceDefaults/ImageMapper.ServiceDefaults.csproj index 7f41dde..79d19ad 100644 --- a/src/ImageMapper.ServiceDefaults/ImageMapper.ServiceDefaults.csproj +++ b/src/ImageMapper.ServiceDefaults/ImageMapper.ServiceDefaults.csproj @@ -10,13 +10,13 @@ - - - - - - - + + + + + + + diff --git a/src/ImageMapper.Tests/ImageMapper.Tests.csproj b/src/ImageMapper.Tests/ImageMapper.Tests.csproj index 0d31192..1ce362d 100644 --- a/src/ImageMapper.Tests/ImageMapper.Tests.csproj +++ b/src/ImageMapper.Tests/ImageMapper.Tests.csproj @@ -9,10 +9,13 @@ - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ImageMapper.Web/ImageMapper.Web.csproj b/src/ImageMapper.Web/ImageMapper.Web.csproj index 459ab4f..a90098d 100644 --- a/src/ImageMapper.Web/ImageMapper.Web.csproj +++ b/src/ImageMapper.Web/ImageMapper.Web.csproj @@ -12,8 +12,8 @@ - - + + From 63fc0c2896b31a843061ee7950bd3c3b6f104dc0 Mon Sep 17 00:00:00 2001 From: Tim Moore Date: Mon, 23 Feb 2026 17:19:12 +0000 Subject: [PATCH 2/5] Restore cluster marker grouping with dynamic load --- README.md | 6 ++---- src/ImageMapper.Web/Components/Pages/Home.razor | 14 +++++++++++--- .../Components/Pages/Home.razor.cs | 17 +++++++++++++---- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bbdb3cf..62ec3f3 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,10 @@ The folder for images can be configured via `appsettings.json` file in ImageMapp ## TODO -- Restore cluster grouping of markers when dynamically loading images - Show progress when loading images -- Potential for optimising Leaflet rendering, only render markers in view etc? Would need back-end to cache image locations and support querying by bounding box? - Abstract file enumeration and loading to allow varied sources not just a file folder -- Caching -- Configure map tile provider options +- Caching. Memory and/or stored cache of processed image metadata to speed up subsequent loads and reduce processing on each request. Would need to detect changes however. +- Configurable map tile provider options? - UI improvements, filtering etc - Error handling and logging improvements - Container support diff --git a/src/ImageMapper.Web/Components/Pages/Home.razor b/src/ImageMapper.Web/Components/Pages/Home.razor index cf6d2b8..226912a 100644 --- a/src/ImageMapper.Web/Components/Pages/Home.razor +++ b/src/ImageMapper.Web/Components/Pages/Home.razor @@ -10,6 +10,8 @@ \ No newline at end of file diff --git a/src/ImageMapper.Web/Components/Pages/Home.razor.cs b/src/ImageMapper.Web/Components/Pages/Home.razor.cs index e3dc646..9ab05ae 100644 --- a/src/ImageMapper.Web/Components/Pages/Home.razor.cs +++ b/src/ImageMapper.Web/Components/Pages/Home.razor.cs @@ -11,10 +11,12 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - // Initialize the map once + // Initialize the map with cluster grouping await JS.InvokeVoidAsync("initClusterMap", cts.Token); - // Add markers as images arrive + // Add markers as images arrive in batches for better performance + int batchSize = 10; + int batchCount = 0; await foreach (ImageInfo? image in imageFetcher.Fetch(cts.Token)) { if (image == null) @@ -28,9 +30,16 @@ await JS.InvokeVoidAsync("addMarkerToMap", image.Longitude, Url = $"/api/images/raw/{Uri.EscapeDataString(image.RelativePath)}" }); - - StateHasChanged(); + + batchCount++; + if (batchCount % batchSize == 0) + { + StateHasChanged(); + } } + + // Final update to ensure UI is synchronized + StateHasChanged(); } } From e28e481b8ceaf4d6d7dd49f297cbab54c4c9d387 Mon Sep 17 00:00:00 2001 From: Tim Moore Date: Mon, 23 Feb 2026 18:18:44 +0000 Subject: [PATCH 3/5] Fix fetch image path issue with subfolders --- README.md | 1 + .../Controllers/ImagesController.cs | 11 +++++++--- .../Exceptions/PathTraversalException.cs | 17 +++++++++++++++ src/ImageMapper.Api/Services/IImageService.cs | 21 +++++++++++++++++++ src/ImageMapper.Api/Services/ImageService.cs | 3 ++- .../Controllers/ImagesController.cs | 5 ++++- 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 src/ImageMapper.Api/Exceptions/PathTraversalException.cs diff --git a/README.md b/README.md index 62ec3f3..ff73224 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ The folder for images can be configured via `appsettings.json` file in ImageMapp ## TODO +- Click to show full size image and optionally metadata details / path - Show progress when loading images - Abstract file enumeration and loading to allow varied sources not just a file folder - Caching. Memory and/or stored cache of processed image metadata to speed up subsequent loads and reduce processing on each request. Would need to detect changes however. diff --git a/src/ImageMapper.Api/Controllers/ImagesController.cs b/src/ImageMapper.Api/Controllers/ImagesController.cs index ac878e2..05f4640 100644 --- a/src/ImageMapper.Api/Controllers/ImagesController.cs +++ b/src/ImageMapper.Api/Controllers/ImagesController.cs @@ -1,7 +1,9 @@ +using ImageMapper.Api.Exceptions; using ImageMapper.Api.Services; using ImageMapper.Models; using Microsoft.AspNetCore.Mvc; using Serilog; +using System.Web; namespace ImageMapper.Api.Controllers; @@ -20,17 +22,20 @@ public IAsyncEnumerable Get(CancellationToken ct) [HttpGet("raw/{**relativePath}")] public async Task GetRaw(string relativePath, CancellationToken ct) { - Log.Debug("GET /api/images/raw/{RelativePath} - Retrieving image", relativePath); + // URL-decode the path to handle encoded path separators (%2F -> /) + var decodedPath = HttpUtility.UrlDecode(relativePath); + + Log.Debug("GET /api/images/raw/{RelativePath} - Retrieving image", decodedPath); try { - var bytes = await _svc.GetImageBytesAsync(relativePath, ct); + var bytes = await _svc.GetImageBytesAsync(decodedPath, ct); if (bytes == null) return NotFound(); return File(bytes, "application/octet-stream"); } - catch (ArgumentException ex) + catch (PathTraversalException ex) { Log.Warning("Path traversal rejected: {Message}", ex.Message); return BadRequest("Invalid path"); diff --git a/src/ImageMapper.Api/Exceptions/PathTraversalException.cs b/src/ImageMapper.Api/Exceptions/PathTraversalException.cs new file mode 100644 index 0000000..8bdc694 --- /dev/null +++ b/src/ImageMapper.Api/Exceptions/PathTraversalException.cs @@ -0,0 +1,17 @@ +namespace ImageMapper.Api.Exceptions; + +/// +/// Thrown when a path traversal attempt is detected. +/// +public class PathTraversalException : ArgumentException +{ + public PathTraversalException() : base("Path traversal detected") { } + + public PathTraversalException(string message) : base(message) { } + + public PathTraversalException(string message, Exception innerException) + : base(message, innerException) { } + + public PathTraversalException(string message, string paramName) + : base(message, paramName) { } +} diff --git a/src/ImageMapper.Api/Services/IImageService.cs b/src/ImageMapper.Api/Services/IImageService.cs index b11d2ba..5019260 100644 --- a/src/ImageMapper.Api/Services/IImageService.cs +++ b/src/ImageMapper.Api/Services/IImageService.cs @@ -1,9 +1,30 @@ +using ImageMapper.Api.Exceptions; using ImageMapper.Models; namespace ImageMapper.Api.Services; public interface IImageService { + /// + /// Asynchronously retrieves a sequence of image information. + /// + /// This method allows for cancellation of the operation through the provided cancellation token. + /// If the operation is canceled, an will be thrown. + /// The cancellation token to observe while waiting for the asynchronous operation to complete. + /// An asynchronous sequence of objects representing the retrieved images. IAsyncEnumerable GetImagesAsync(CancellationToken ct = default); + + /// + /// Asynchronously retrieves the image data as a byte array from the specified relative path. + /// + /// This method is intended for use in scenarios where image data needs to be loaded + /// asynchronously, such as in UI applications. Ensure that the relative path is correctly specified to avoid + /// errors. + /// The relative path to the image file. This path must be valid and accessible; otherwise, an exception may be + /// thrown. + /// A cancellation token that can be used to cancel the operation. The default value is CancellationToken.None. + /// A task that represents the asynchronous operation. The task result contains a byte array of the image data, or + /// null if the image could not be found. + /// Thrown when the provided relative path is invalid or attempts to traverse outside the allowed directory." Task GetImageBytesAsync(string relativePath, CancellationToken ct = default); } diff --git a/src/ImageMapper.Api/Services/ImageService.cs b/src/ImageMapper.Api/Services/ImageService.cs index 47c1934..94b878a 100644 --- a/src/ImageMapper.Api/Services/ImageService.cs +++ b/src/ImageMapper.Api/Services/ImageService.cs @@ -1,3 +1,4 @@ +using ImageMapper.Api.Exceptions; using ImageMapper.Models; using MetadataExtractor; using MetadataExtractor.Formats.Exif; @@ -74,7 +75,7 @@ public async IAsyncEnumerable GetImagesAsync([EnumeratorCancellation] // Validate that the resolved path is within the root directory if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException("Path traversal detected", nameof(relativePath)); + throw new PathTraversalException("Path traversal detected", nameof(relativePath)); } if (!File.Exists(fullPath)) diff --git a/src/ImageMapper.Web/Controllers/ImagesController.cs b/src/ImageMapper.Web/Controllers/ImagesController.cs index abf972c..9cd2048 100644 --- a/src/ImageMapper.Web/Controllers/ImagesController.cs +++ b/src/ImageMapper.Web/Controllers/ImagesController.cs @@ -1,5 +1,6 @@ using ImageMapper.Web.Client; using Microsoft.AspNetCore.Mvc; +using System.Web; namespace ImageMapper.Web.Controllers { @@ -13,7 +14,9 @@ public async Task GetRaw(string relativePath, CancellationToken c if (string.IsNullOrWhiteSpace(relativePath)) return BadRequest("Relative path cannot be empty."); - var stream = await imageFetcher.FetchRawImageStream(relativePath, ct); + // URL-decode the path to handle encoded path separators (%2F -> /) + var decodedPath = HttpUtility.UrlDecode(relativePath); + var stream = await imageFetcher.FetchRawImageStream(decodedPath, ct); if (stream == null) return NotFound(); From 1fdff139d6db437f5f3ffc85475274cb2c9393b8 Mon Sep 17 00:00:00 2001 From: Tim Moore Date: Mon, 23 Feb 2026 18:32:06 +0000 Subject: [PATCH 4/5] GitHub action workaround for SSL trust issue in .NET 10.0.103 https://github.com/actions/runner-images/issues/13705 https://github.com/dotnet/aspnetcore/issues/65391 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ad5953..3b9f590 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: 10.0.x + dotnet-version: 10.0.102 - name: Restore dependencies run: dotnet restore From d79a3ed3da51b5a3a06ba4bfb4bc268fd32b43df Mon Sep 17 00:00:00 2001 From: Tim Moore Date: Mon, 23 Feb 2026 18:38:31 +0000 Subject: [PATCH 5/5] Update unit tests for change in expected exception --- src/ImageMapper.Tests/ImagesApiTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageMapper.Tests/ImagesApiTest.cs b/src/ImageMapper.Tests/ImagesApiTest.cs index b2161ff..bbebfe7 100644 --- a/src/ImageMapper.Tests/ImagesApiTest.cs +++ b/src/ImageMapper.Tests/ImagesApiTest.cs @@ -1,3 +1,4 @@ +using ImageMapper.Api.Exceptions; using ImageMapper.Api.Services; using ImageMapper.Models; using Microsoft.Extensions.Configuration; @@ -56,7 +57,7 @@ public void GetImageBytesAsyncRejectsPathTraversalAttempts(string traversalPath) var service = new ImageService(config); // Act & Assert - var ex = Assert.ThrowsAsync( + var ex = Assert.ThrowsAsync( async () => await service.GetImageBytesAsync(traversalPath)); Assert.That(ex.ParamName, Is.EqualTo("relativePath"));