fetch_ml/internal/api/datasets/handlers.go
Jeremie Fraeys 420de879ff
feat(api): integrate scheduler protocol and WebSocket enhancements
Update API layer for scheduler integration:
- WebSocket handlers with scheduler protocol support
- Jobs WebSocket endpoint with priority queue integration
- Validation middleware for scheduler messages
- Server configuration with security hardening
- Protocol definitions for worker-scheduler communication
- Dataset handlers with tenant isolation checks
- Response helpers with audit context
- OpenAPI spec updates for new endpoints
2026-02-26 12:05:57 -05:00

198 lines
5.4 KiB
Go

// Package datasets provides WebSocket handlers for dataset-related operations
package datasets
import (
"context"
"encoding/binary"
"encoding/json"
"time"
"github.com/gorilla/websocket"
"github.com/jfraeys/fetch_ml/internal/auth"
"github.com/jfraeys/fetch_ml/internal/logging"
"github.com/jfraeys/fetch_ml/internal/storage"
)
// Handler provides dataset-related WebSocket handlers
type Handler struct {
logger *logging.Logger
db *storage.DB
dataDir string
}
// NewHandler creates a new datasets handler
func NewHandler(
logger *logging.Logger,
db *storage.DB,
dataDir string,
) *Handler {
return &Handler{
logger: logger,
db: db,
dataDir: dataDir,
}
}
// Error codes
const (
ErrorCodeInvalidRequest = 0x01
ErrorCodeAuthenticationFailed = 0x02
ErrorCodePermissionDenied = 0x03
ErrorCodeResourceNotFound = 0x04
)
// sendErrorPacket sends an error response packet to the client
func sendErrorPacket(conn *websocket.Conn, message string) error {
err := map[string]any{
"error": true,
"code": ErrorCodeInvalidRequest,
"message": message,
}
return conn.WriteJSON(err)
}
// sendSuccessPacket sends a success response packet
func (h *Handler) sendSuccessPacket(conn *websocket.Conn, data map[string]any) error {
return conn.WriteJSON(data)
}
// sendDataPacket sends a data response packet
func (h *Handler) sendDataPacket(conn *websocket.Conn, dataType string, payload []byte) error {
return conn.WriteJSON(map[string]any{
"type": dataType,
"payload": string(payload),
})
}
// HandleDatasetList handles listing datasets
// Protocol: [api_key_hash:16]
func (h *Handler) HandleDatasetList(conn *websocket.Conn, payload []byte, user *auth.User) error {
h.logger.Info("listing datasets", "user", user.Name)
var datasets []*storage.Dataset
if h.db != nil {
var err error
datasets, err = h.db.ListDatasets(context.Background(), 100)
if err != nil {
h.logger.Warn("failed to list datasets from db", "error", err)
datasets = []*storage.Dataset{}
}
}
data, _ := json.Marshal(datasets)
return h.sendDataPacket(conn, "datasets", data)
}
// HandleDatasetRegister handles registering a new dataset
// Protocol: [api_key_hash:16][name_len:1][name:var][path_len:2][path:var]
func (h *Handler) HandleDatasetRegister(
conn *websocket.Conn, payload []byte, user *auth.User,
) error {
if len(payload) < 16+1+2 {
return sendErrorPacket(conn, "register dataset payload too short")
}
offset := 16
nameLen := int(payload[offset])
offset++
if nameLen <= 0 || len(payload) < offset+nameLen+2 {
return sendErrorPacket(conn, "invalid name length")
}
name := string(payload[offset : offset+nameLen])
offset += nameLen
pathLen := int(binary.BigEndian.Uint16(payload[offset : offset+2]))
offset += 2
if pathLen < 0 || len(payload) < offset+pathLen {
return sendErrorPacket(conn, "invalid path length")
}
path := string(payload[offset : offset+pathLen])
h.logger.Info("registering dataset", "name", name, "path", path, "user", user.Name)
// Save to database if available
if h.db != nil {
ds := &storage.Dataset{
Name: name,
URL: path,
}
if err := h.db.UpsertDataset(context.Background(), ds); err != nil {
h.logger.Warn("failed to save dataset to db", "error", err, "name", name)
}
}
return h.sendSuccessPacket(conn, map[string]any{
"success": true,
"name": name,
"path": path,
"user": user.Name,
"time": time.Now().UTC(),
})
}
// HandleDatasetInfo handles getting dataset info
// Protocol: [api_key_hash:16][dataset_id_len:1][dataset_id:var]
func (h *Handler) HandleDatasetInfo(conn *websocket.Conn, payload []byte, user *auth.User) error {
if len(payload) < 16+1 {
return sendErrorPacket(conn, "dataset info payload too short")
}
offset := 16
datasetIDLen := int(payload[offset])
offset++
if datasetIDLen <= 0 || len(payload) < offset+datasetIDLen {
return sendErrorPacket(conn, "invalid dataset ID length")
}
datasetID := string(payload[offset : offset+datasetIDLen])
h.logger.Info("getting dataset info", "dataset_id", datasetID, "user", user.Name)
// Query database if available
if h.db != nil {
ds, err := h.db.GetDataset(context.Background(), datasetID)
if err == nil && ds != nil {
data, _ := json.Marshal(ds)
return h.sendDataPacket(conn, "dataset_info", data)
}
if err != nil {
h.logger.Warn("failed to get dataset from db", "error", err, "name", datasetID)
}
}
return h.sendDataPacket(conn, "dataset_info", []byte("{}"))
}
// HandleDatasetSearch handles searching datasets
// Protocol: [api_key_hash:16][query_len:2][query:var]
func (h *Handler) HandleDatasetSearch(conn *websocket.Conn, payload []byte, user *auth.User) error {
if len(payload) < 16+2 {
return sendErrorPacket(conn, "dataset search payload too short")
}
offset := 16
queryLen := int(binary.BigEndian.Uint16(payload[offset : offset+2]))
offset += 2
if queryLen < 0 || len(payload) < offset+queryLen {
return sendErrorPacket(conn, "invalid query length")
}
query := string(payload[offset : offset+queryLen])
h.logger.Info("searching datasets", "query", query, "user", user.Name)
// Search database if available
var datasets []*storage.Dataset
if h.db != nil {
var err error
datasets, err = h.db.SearchDatasets(context.Background(), query, 100)
if err != nil {
h.logger.Warn("failed to search datasets in db", "error", err, "query", query)
datasets = []*storage.Dataset{}
}
}
data, _ := json.Marshal(datasets)
return h.sendDataPacket(conn, "datasets", data)
}