248 lines
8.2 KiB
Go
248 lines
8.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/metazla/meta-core/internal/config"
|
|
"github.com/metazla/meta-core/internal/discovery"
|
|
"github.com/metazla/meta-core/internal/leader"
|
|
"github.com/metazla/meta-core/internal/mounts"
|
|
"github.com/metazla/meta-core/internal/storage"
|
|
"github.com/metazla/meta-core/internal/watcher"
|
|
)
|
|
|
|
// Server is the HTTP API server for meta-core
|
|
type Server struct {
|
|
config *config.Config
|
|
election *leader.Election
|
|
discovery *discovery.Service
|
|
cleaner *discovery.Cleaner
|
|
storage *storage.Client
|
|
mountsManager *mounts.Manager
|
|
mountsHandlers *mounts.Handlers
|
|
mountPoller *mounts.Poller
|
|
watcherDispatcher *watcher.Dispatcher
|
|
fileWatcher *watcher.Watcher
|
|
watcherHandlers *watcher.Handlers
|
|
router *mux.Router
|
|
server *http.Server
|
|
}
|
|
|
|
// NewServer creates a new API server
|
|
func NewServer(
|
|
cfg *config.Config,
|
|
election *leader.Election,
|
|
disc *discovery.Service,
|
|
cleaner *discovery.Cleaner,
|
|
stor *storage.Client,
|
|
) *Server {
|
|
s := &Server{
|
|
config: cfg,
|
|
election: election,
|
|
discovery: disc,
|
|
cleaner: cleaner,
|
|
storage: stor,
|
|
router: mux.NewRouter(),
|
|
}
|
|
|
|
// Initialize file watcher (if enabled) - before mounts so we can pass scanFunc
|
|
var scanFunc mounts.ScanFunc
|
|
if cfg.EnableFileWatcher && len(cfg.WatchFolderList) > 0 {
|
|
s.watcherDispatcher = watcher.NewDispatcher()
|
|
fileWatcher, err := watcher.NewWatcher(cfg, s.watcherDispatcher)
|
|
if err != nil {
|
|
log.Printf("[API] Warning: failed to initialize file watcher: %v", err)
|
|
} else {
|
|
s.fileWatcher = fileWatcher
|
|
s.watcherHandlers = watcher.NewHandlers(fileWatcher, s.watcherDispatcher)
|
|
// Create scan function from watcher
|
|
scanFunc = fileWatcher.ScanMountPath
|
|
}
|
|
}
|
|
|
|
// Initialize mounts manager
|
|
mountsManager, err := mounts.NewManager(cfg)
|
|
if err != nil {
|
|
log.Printf("[API] Warning: failed to initialize mounts manager: %v", err)
|
|
} else {
|
|
s.mountsManager = mountsManager
|
|
s.mountsHandlers = mounts.NewHandlers(mountsManager, scanFunc)
|
|
|
|
// Create and configure mount poller
|
|
s.mountPoller = mounts.NewPoller(mountsManager, scanFunc)
|
|
mountsManager.SetPoller(s.mountPoller)
|
|
}
|
|
|
|
s.setupRoutes()
|
|
return s
|
|
}
|
|
|
|
// setupRoutes configures all API routes
|
|
func (s *Server) setupRoutes() {
|
|
// Health and status
|
|
s.router.HandleFunc("/health", s.handleHealth).Methods("GET")
|
|
s.router.HandleFunc("/status", s.handleStatus).Methods("GET")
|
|
s.router.HandleFunc("/leader", s.handleLeader).Methods("GET")
|
|
s.router.HandleFunc("/role", s.handleRole).Methods("GET")
|
|
s.router.HandleFunc("/urls", s.handleURLs).Methods("GET")
|
|
|
|
// Metadata Editor API routes (must be before /meta/{hash} routes)
|
|
s.router.HandleFunc("/api/metadata/hash-ids", s.handleGetHashIds).Methods("GET")
|
|
s.router.HandleFunc("/api/metadata/list", s.handleListMetadata).Methods("GET")
|
|
s.router.HandleFunc("/api/metadata/search", s.handleSearchMetadata).Methods("POST")
|
|
s.router.HandleFunc("/api/metadata/batch", s.handleBatchUpdate).Methods("POST")
|
|
s.router.HandleFunc("/api/metadata/clear", s.handleClearMetadata).Methods("POST")
|
|
s.router.HandleFunc("/api/metadata/{hashId}/property", s.handleMetadataGetProperty).Methods("GET")
|
|
s.router.HandleFunc("/api/metadata/{hashId}/property", s.handleMetadataUpdateProperty).Methods("PUT")
|
|
s.router.HandleFunc("/api/metadata/{hashId}", s.handleGetMetadataByHashId).Methods("GET")
|
|
s.router.HandleFunc("/api/metadata/{hashId}", s.handleUpdateMetadataByHashId).Methods("PUT")
|
|
s.router.HandleFunc("/api/metadata/{hashId}", s.handleDeleteMetadataByHashId).Methods("DELETE")
|
|
|
|
// KV Browser API routes
|
|
s.router.HandleFunc("/api/kv/info", s.handleKVInfo).Methods("GET")
|
|
s.router.HandleFunc("/api/kv/keys", s.handleKVKeys).Methods("GET")
|
|
s.router.HandleFunc("/api/kv/key/{key:.*}", s.handleKVGetKey).Methods("GET")
|
|
|
|
// Metadata operations - base endpoints
|
|
s.router.HandleFunc("/meta/{hash}", s.handleGetMeta).Methods("GET")
|
|
s.router.HandleFunc("/meta/{hash}", s.handlePutMeta).Methods("PUT")
|
|
s.router.HandleFunc("/meta/{hash}", s.handlePatchMeta).Methods("PATCH")
|
|
s.router.HandleFunc("/meta/{hash}", s.handleDeleteMeta).Methods("DELETE")
|
|
s.router.HandleFunc("/meta", s.handleListMeta).Methods("GET")
|
|
|
|
// Metadata operations - set operations (must be before property routes)
|
|
s.router.HandleFunc("/meta/{hash}/_add/{key:.*}", s.handleAddToSet).Methods("POST")
|
|
|
|
// Metadata operations - property-level (key can contain slashes)
|
|
s.router.HandleFunc("/meta/{hash}/{key:.*}", s.handleGetProperty).Methods("GET")
|
|
s.router.HandleFunc("/meta/{hash}/{key:.*}", s.handlePutProperty).Methods("PUT")
|
|
s.router.HandleFunc("/meta/{hash}/{key:.*}", s.handleDeleteProperty).Methods("DELETE")
|
|
|
|
// Data operations
|
|
s.router.HandleFunc("/data/{hash}/path", s.handleGetDataPath).Methods("GET")
|
|
s.router.HandleFunc("/data/{hash}", s.handleHeadData).Methods("HEAD")
|
|
|
|
// File operations (by CID)
|
|
s.router.HandleFunc("/file/{cid}", s.handleGetFileByCID).Methods("GET")
|
|
s.router.HandleFunc("/file/cid", s.handleComputeFileCID).Methods("POST")
|
|
|
|
// Service discovery
|
|
s.router.HandleFunc("/services", s.handleListServices).Methods("GET")
|
|
s.router.HandleFunc("/services/cleanup/stats", s.handleCleanupStats).Methods("GET")
|
|
s.router.HandleFunc("/services/{name}", s.handleGetService).Methods("GET")
|
|
|
|
// Mount management routes (if manager initialized)
|
|
if s.mountsHandlers != nil {
|
|
s.mountsHandlers.RegisterRoutes(s.router)
|
|
log.Println("[API] Mount management routes registered at /api/mounts/*")
|
|
}
|
|
|
|
log.Println("[API] Metadata Editor routes registered at /api/metadata/*")
|
|
log.Println("[API] KV Browser routes registered at /api/kv/*")
|
|
|
|
// File watcher routes (if watcher initialized)
|
|
if s.watcherHandlers != nil {
|
|
s.watcherHandlers.RegisterRoutes(s.router)
|
|
log.Println("[API] File watcher routes registered at /api/events/*, /api/scan/*")
|
|
}
|
|
|
|
// Add middleware
|
|
s.router.Use(loggingMiddleware)
|
|
s.router.Use(corsMiddleware)
|
|
}
|
|
|
|
// Start starts the HTTP server
|
|
func (s *Server) Start() error {
|
|
addr := fmt.Sprintf("%s:%d", s.config.HTTPHost, s.config.HTTPPort)
|
|
|
|
s.server = &http.Server{
|
|
Addr: addr,
|
|
Handler: s.router,
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
log.Printf("[API] Starting HTTP server on %s", addr)
|
|
|
|
go func() {
|
|
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Printf("[API] HTTP server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Start file watcher (if initialized)
|
|
if s.fileWatcher != nil {
|
|
if err := s.fileWatcher.Start(); err != nil {
|
|
log.Printf("[API] Warning: failed to start file watcher: %v", err)
|
|
}
|
|
}
|
|
|
|
// Start mount poller (if initialized)
|
|
if s.mountPoller != nil {
|
|
if err := s.mountPoller.Start(); err != nil {
|
|
log.Printf("[API] Warning: failed to start mount poller: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully stops the HTTP server
|
|
func (s *Server) Stop() error {
|
|
// Stop mount poller
|
|
if s.mountPoller != nil {
|
|
if err := s.mountPoller.Stop(); err != nil {
|
|
log.Printf("[API] Warning: failed to stop mount poller: %v", err)
|
|
}
|
|
}
|
|
|
|
// Stop file watcher
|
|
if s.fileWatcher != nil {
|
|
if err := s.fileWatcher.Stop(); err != nil {
|
|
log.Printf("[API] Warning: failed to stop file watcher: %v", err)
|
|
}
|
|
}
|
|
|
|
if s.server == nil {
|
|
return nil
|
|
}
|
|
|
|
log.Println("[API] Stopping HTTP server...")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
return s.server.Shutdown(ctx)
|
|
}
|
|
|
|
// loggingMiddleware logs all requests
|
|
func loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
next.ServeHTTP(w, r)
|
|
log.Printf("[API] %s %s %s", r.Method, r.URL.Path, time.Since(start))
|
|
})
|
|
}
|
|
|
|
// corsMiddleware adds CORS headers
|
|
func corsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|