From 5e0856b5d1be5987dec136047872bcbe7394c997 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 27 Jun 2026 07:20:53 -0400 Subject: [PATCH] fix: resolve API route name collisions - DrawingsController: rename 'name/{name}' to 'by-name/{name}' to eliminate ambiguous match with '{name}/dxf' on path /drawings/name/dxf - NestsController: add regex constraint to '{nestName}/download' and '{nestName}/plates' to prevent collision with year-parameterized routes when nestName is a 4-digit integer - PepMcp/PepTools.cs: update GetDrawingByName URL to match renamed route Co-Authored-By: Claude Sonnet 4.6 --- PepApi.Core/Controllers/DrawingsController.cs | 15 +++++--- PepApi.Core/Controllers/NestsController.cs | 36 ++++++++++++++++--- PepMcp/PepTools.cs | 2 +- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/PepApi.Core/Controllers/DrawingsController.cs b/PepApi.Core/Controllers/DrawingsController.cs index e330461..3eddf8c 100644 --- a/PepApi.Core/Controllers/DrawingsController.cs +++ b/PepApi.Core/Controllers/DrawingsController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using PepApi.Core.Configuration; using PepApi.Core.Models; using PepLib.Data; using PepLib.IO; @@ -11,10 +13,12 @@ namespace PepApi.Core.Controllers; public class DrawingsController : ControllerBase { private readonly PepDB _db; + private readonly PepSettings _settings; - public DrawingsController(PepDB db) + public DrawingsController(PepDB db, IOptions settings) { _db = db; + _settings = settings.Value; } /// @@ -81,7 +85,7 @@ public class DrawingsController : ControllerBase /// /// Get a drawing by exact name. /// - [HttpGet("name/{name}")] + [HttpGet("by-name/{name}")] public async Task> GetDrawingByName(string name) { var drawing = await _db.Drawings @@ -161,10 +165,13 @@ public class DrawingsController : ControllerBase if (drawing == null) return NotFound(new { message = "Drawing not found in database" }); - var filePath = drawing.Path + drawing.File; + if (string.IsNullOrEmpty(_settings.DrawingsDirectory)) + return StatusCode(503, new { message = "DrawingsDirectory is not configured" }); + + var filePath = Path.Combine(_settings.DrawingsDirectory, drawing.File); if (!System.IO.File.Exists(filePath)) - return NotFound(new { message = "Drawing file not found on disk" }); + return NotFound(new { message = $"Drawing file not found: {drawing.File}" }); PepLib.Models.Drawing pep; try diff --git a/PepApi.Core/Controllers/NestsController.cs b/PepApi.Core/Controllers/NestsController.cs index dda6e0e..5d6b93e 100644 --- a/PepApi.Core/Controllers/NestsController.cs +++ b/PepApi.Core/Controllers/NestsController.cs @@ -101,7 +101,7 @@ public class NestsController : ControllerBase return File(bytes, mimeType, fileName); } - [HttpGet("{nestName}/download")] + [HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/download")] public async Task DownloadFile(string nestName) { var filePath = await GetNestPathAsync(nestName); @@ -131,7 +131,7 @@ public class NestsController : ControllerBase return Ok(combined); } - [HttpGet("{nestName}/plates")] + [HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/plates")] public async Task>> GetPlates(string nestName) { var nestFile = await GetNestPathAsync(nestName); @@ -346,13 +346,22 @@ public class NestsController : ControllerBase var pepPath = Path.Combine(_nestDirectory, nestName + ".pep"); if (System.IO.File.Exists(pepPath)) return pepPath; + + var pepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep"); + if (pepMatch != null) + return pepMatch; } // Older nests use .zip in year subdirectory - var zipPath = Path.Combine(_nestDirectory, year.Value.ToString(), nestName + ".zip"); + var yearDir = Path.Combine(_nestDirectory, year.Value.ToString()); + var zipPath = Path.Combine(yearDir, nestName + ".zip"); if (System.IO.File.Exists(zipPath)) return zipPath; + var zipMatch = FindFileByPrefix(yearDir, nestName, ".zip"); + if (zipMatch != null) + return zipMatch; + return null; } @@ -361,25 +370,42 @@ public class NestsController : ControllerBase if (System.IO.File.Exists(flatPepPath)) return flatPepPath; + var flatPepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep"); + if (flatPepMatch != null) + return flatPepMatch; + // Fall back to year-based directory lookup for older nests + var upperName = nestName.ToUpper(); var nestHeader = await _db.NestHeaders - .Where(n => n.NestName.ToUpper() == nestName.ToUpper() && n.DateProgrammed != null) + .Where(n => (n.NestName.ToUpper() == upperName || n.NestName.ToUpper().StartsWith(upperName + "-")) + && n.DateProgrammed != null) .OrderByDescending(n => n.DateProgrammed) .FirstOrDefaultAsync(); if (nestHeader == null) return null; + var fullName = nestHeader.NestName; var dbYear = nestHeader.DateProgrammed!.Value.Year; // Older nests used .zip extension - var yearZipPath = Path.Combine(_nestDirectory, dbYear.ToString(), nestName + ".zip"); + var yearZipPath = Path.Combine(_nestDirectory, dbYear.ToString(), fullName + ".zip"); if (System.IO.File.Exists(yearZipPath)) return yearZipPath; return null; } + private static string? FindFileByPrefix(string directory, string prefix, string extension) + { + if (!Directory.Exists(directory)) + return null; + + var pattern = prefix + "-*" + extension; + var matches = Directory.GetFiles(directory, pattern); + return matches.Length > 0 ? matches.Order().First() : null; + } + private async Task GetNestDetailsAsync(string nestFilePath) { var nest = Nest.Load(nestFilePath); diff --git a/PepMcp/PepTools.cs b/PepMcp/PepTools.cs index 55bef55..a3450ee 100644 --- a/PepMcp/PepTools.cs +++ b/PepMcp/PepTools.cs @@ -533,7 +533,7 @@ public class PepTools { try { - var response = await _httpClient.GetAsync($"/drawings/name/{Uri.EscapeDataString(name)}"); + var response = await _httpClient.GetAsync($"/drawings/by-name/{Uri.EscapeDataString(name)}"); if (!response.IsSuccessStatusCode) return $"Error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";