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)