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 <noreply@anthropic.com>
This commit is contained in:
aj
2026-06-27 07:20:53 -04:00
co-authored by Claude Sonnet 4.6
parent 9fcc9df209
commit 5e0856b5d1
3 changed files with 43 additions and 10 deletions
+11 -4
View File
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using PepApi.Core.Configuration;
using PepApi.Core.Models; using PepApi.Core.Models;
using PepLib.Data; using PepLib.Data;
using PepLib.IO; using PepLib.IO;
@@ -11,10 +13,12 @@ namespace PepApi.Core.Controllers;
public class DrawingsController : ControllerBase public class DrawingsController : ControllerBase
{ {
private readonly PepDB _db; private readonly PepDB _db;
private readonly PepSettings _settings;
public DrawingsController(PepDB db) public DrawingsController(PepDB db, IOptions<PepSettings> settings)
{ {
_db = db; _db = db;
_settings = settings.Value;
} }
/// <summary> /// <summary>
@@ -81,7 +85,7 @@ public class DrawingsController : ControllerBase
/// <summary> /// <summary>
/// Get a drawing by exact name. /// Get a drawing by exact name.
/// </summary> /// </summary>
[HttpGet("name/{name}")] [HttpGet("by-name/{name}")]
public async Task<ActionResult<DrawingDetails>> GetDrawingByName(string name) public async Task<ActionResult<DrawingDetails>> GetDrawingByName(string name)
{ {
var drawing = await _db.Drawings var drawing = await _db.Drawings
@@ -161,10 +165,13 @@ public class DrawingsController : ControllerBase
if (drawing == null) if (drawing == null)
return NotFound(new { message = "Drawing not found in database" }); 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)) 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; PepLib.Models.Drawing pep;
try try
+31 -5
View File
@@ -101,7 +101,7 @@ public class NestsController : ControllerBase
return File(bytes, mimeType, fileName); return File(bytes, mimeType, fileName);
} }
[HttpGet("{nestName}/download")] [HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/download")]
public async Task<IActionResult> DownloadFile(string nestName) public async Task<IActionResult> DownloadFile(string nestName)
{ {
var filePath = await GetNestPathAsync(nestName); var filePath = await GetNestPathAsync(nestName);
@@ -131,7 +131,7 @@ public class NestsController : ControllerBase
return Ok(combined); return Ok(combined);
} }
[HttpGet("{nestName}/plates")] [HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/plates")]
public async Task<ActionResult<List<Plate>>> GetPlates(string nestName) public async Task<ActionResult<List<Plate>>> GetPlates(string nestName)
{ {
var nestFile = await GetNestPathAsync(nestName); var nestFile = await GetNestPathAsync(nestName);
@@ -346,13 +346,22 @@ public class NestsController : ControllerBase
var pepPath = Path.Combine(_nestDirectory, nestName + ".pep"); var pepPath = Path.Combine(_nestDirectory, nestName + ".pep");
if (System.IO.File.Exists(pepPath)) if (System.IO.File.Exists(pepPath))
return pepPath; return pepPath;
var pepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep");
if (pepMatch != null)
return pepMatch;
} }
// Older nests use .zip in year subdirectory // 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)) if (System.IO.File.Exists(zipPath))
return zipPath; return zipPath;
var zipMatch = FindFileByPrefix(yearDir, nestName, ".zip");
if (zipMatch != null)
return zipMatch;
return null; return null;
} }
@@ -361,25 +370,42 @@ public class NestsController : ControllerBase
if (System.IO.File.Exists(flatPepPath)) if (System.IO.File.Exists(flatPepPath))
return flatPepPath; return flatPepPath;
var flatPepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep");
if (flatPepMatch != null)
return flatPepMatch;
// Fall back to year-based directory lookup for older nests // Fall back to year-based directory lookup for older nests
var upperName = nestName.ToUpper();
var nestHeader = await _db.NestHeaders 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) .OrderByDescending(n => n.DateProgrammed)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (nestHeader == null) if (nestHeader == null)
return null; return null;
var fullName = nestHeader.NestName;
var dbYear = nestHeader.DateProgrammed!.Value.Year; var dbYear = nestHeader.DateProgrammed!.Value.Year;
// Older nests used .zip extension // 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)) if (System.IO.File.Exists(yearZipPath))
return yearZipPath; return yearZipPath;
return null; 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<NestDetails> GetNestDetailsAsync(string nestFilePath) private async Task<NestDetails> GetNestDetailsAsync(string nestFilePath)
{ {
var nest = Nest.Load(nestFilePath); var nest = Nest.Load(nestFilePath);
+1 -1
View File
@@ -533,7 +533,7 @@ public class PepTools
{ {
try 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) if (!response.IsSuccessStatusCode)
return $"Error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}"; return $"Error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";