131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
package watcher
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// Handlers provides HTTP handlers for watcher operations
|
|
type Handlers struct {
|
|
watcher *Watcher
|
|
dispatcher *Dispatcher
|
|
}
|
|
|
|
// NewHandlers creates new watcher handlers
|
|
func NewHandlers(watcher *Watcher, dispatcher *Dispatcher) *Handlers {
|
|
return &Handlers{
|
|
watcher: watcher,
|
|
dispatcher: dispatcher,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers all watcher-related routes
|
|
func (h *Handlers) RegisterRoutes(r *mux.Router) {
|
|
// Event polling (for services that don't consume the SSE stream)
|
|
r.HandleFunc("/api/events/poll", h.handlePoll).Methods("GET")
|
|
|
|
// Deprecated scan endpoints - redirect to /api/watchers
|
|
r.HandleFunc("/api/scan/trigger", h.handleTriggerScanDeprecated).Methods("POST")
|
|
r.HandleFunc("/api/scan/status", h.handleScanStatusDeprecated).Methods("GET")
|
|
}
|
|
|
|
// handlePoll handles GET /api/events/poll.
|
|
//
|
|
// DEPRECATED. The long-poll variant over file:events has been superseded by
|
|
// the SSE endpoint at /api/events/files. New consumers should use SSE. This
|
|
// handler stays for one release with a Deprecation header before removal —
|
|
// see docs/api-mediated-access.md PR A scope.
|
|
func (h *Handlers) handlePoll(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Deprecation", "true")
|
|
w.Header().Set("Link", `</api/events/files>; rel="successor-version"`)
|
|
w.Header().Set("Sunset", "after next release")
|
|
// Get since parameter
|
|
sinceStr := r.URL.Query().Get("since")
|
|
sinceMS := int64(0)
|
|
if sinceStr != "" {
|
|
if parsed, err := strconv.ParseInt(sinceStr, 10, 64); err == nil {
|
|
sinceMS = parsed
|
|
}
|
|
}
|
|
|
|
// Get limit parameter
|
|
limitStr := r.URL.Query().Get("limit")
|
|
limit := 100
|
|
if limitStr != "" {
|
|
if parsed, err := strconv.Atoi(limitStr); err == nil {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
// Read events from the `file:events` stream rather than the
|
|
// in-memory buffer. The buffer is fed by the unused debouncer path,
|
|
// so it is always empty in practice — the stream is the source of truth.
|
|
events, err := h.dispatcher.GetRecentEvents(sinceMS, limit)
|
|
if err != nil {
|
|
log.Printf("[watcher.handlePoll] failed to read stream: %v", err)
|
|
events = []FileEvent{}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, EventsListResponse{
|
|
Events: events,
|
|
Count: len(events),
|
|
})
|
|
}
|
|
|
|
// handleTriggerScanDeprecated handles POST /api/scan/trigger (deprecated)
|
|
func (h *Handlers) handleTriggerScanDeprecated(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"status": "ok",
|
|
"message": "Use POST /api/watchers/scan-all instead. This endpoint is deprecated.",
|
|
})
|
|
}
|
|
|
|
// handleScanStatusDeprecated handles GET /api/scan/status (deprecated)
|
|
func (h *Handlers) handleScanStatusDeprecated(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"status": "deprecated",
|
|
"message": "Use GET /api/watchers instead. This endpoint is deprecated.",
|
|
})
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
// writeError writes the typed error envelope (see api/handlers.go for the
|
|
// canonical definition + slug vocabulary). Kept local to avoid an import
|
|
// cycle with the api package.
|
|
func writeError(w http.ResponseWriter, status int, message string) {
|
|
slug, retryable := slugForStatus(status)
|
|
writeJSON(w, status, map[string]interface{}{
|
|
"error": slug,
|
|
"message": message,
|
|
"retryable": retryable,
|
|
})
|
|
}
|
|
|
|
func slugForStatus(status int) (string, bool) {
|
|
switch status {
|
|
case http.StatusConflict:
|
|
return "alias_collision", false
|
|
case http.StatusNotFound:
|
|
return "unknown_root", false
|
|
case http.StatusBadRequest:
|
|
return "schema_violation", false
|
|
case http.StatusServiceUnavailable:
|
|
return "storage_unavailable", true
|
|
case http.StatusInternalServerError:
|
|
return "internal", true
|
|
default:
|
|
return "internal", status >= 500
|
|
}
|
|
}
|