From 5b81e19f2fdf1f0d1ff27a4beb9e69d00213ab9c Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 18 Oct 2026 18:02:25 -0400 Subject: [PATCH] feat(scanner): POST /api/scanner/cancel/:jobId (admin) Cancels a pending or running scan job. Responses: 200 with the job id and status on acceptance, 404 for unknown jobs, 409 for jobs already in a terminal state (completed/failed/cancelled). Additive route in the existing admin scanner group; no contract pins affected. --- internal/handlers/scanner.go | 23 +++++++++++++++++++++++ internal/router/scanner.go | 1 + 2 files changed, 24 insertions(+) diff --git a/internal/handlers/scanner.go b/internal/handlers/scanner.go index 66f902ed..c65d820e 100644 --- a/internal/handlers/scanner.go +++ b/internal/handlers/scanner.go @@ -3,6 +3,7 @@ package handlers import ( "bookhoard/internal/services" "context" + "errors" "fmt" "net/http" @@ -209,6 +210,28 @@ func (h *Handler) GetScanSummary(c *echo.Context) error { }) } +// CancelScan handles POST /api/scanner/cancel/:jobId (admin): requests +// cancellation of a pending or running scan job. Cancellation is +// cooperative — the file currently being processed finishes, the walk +// stops at its next context check, and the archive pass is skipped so +// unvisited files are never treated as missing. +func (h *Handler) CancelScan(c *echo.Context) error { + jobID := c.Param("jobId") + + if err := h.worker.CancelJob(jobID); err != nil { + if errors.Is(err, services.ErrJobNotFound) { + return c.JSON(http.StatusNotFound, map[string]string{"error": "job not found"}) + } + return c.JSON(http.StatusConflict, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "scan cancellation requested", + "job_id": jobID, + "status": "cancelled", + }) +} + // StartWatchModeForLibrary starts watching a specific library's folders func (h *Handler) StartWatchModeForLibrary(ctx context.Context, libraryID pgtype.UUID, adminID pgtype.UUID) error { h.mu.Lock() diff --git a/internal/router/scanner.go b/internal/router/scanner.go index d790e2b1..af3c26c0 100644 --- a/internal/router/scanner.go +++ b/internal/router/scanner.go @@ -17,6 +17,7 @@ func registerScannerRoutes(protected, admin *echo.Group, h *handlers.Handler) { admin.POST("/scanner/scan", h.ScanLibrary) admin.POST("/scanner/start", h.StartScanner) admin.POST("/scanner/stop", h.StopScanner) + admin.POST("/scanner/cancel/:jobId", h.CancelScan) admin.GET("/scanner/status/:jobId", h.GetScanStatus) admin.GET("/scanner/active", h.GetActiveScans)