improve editor and add type detection
This commit is contained in:
@@ -340,6 +340,63 @@ func (s *Server) handleHeadData(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// handleGetFileByCIDInfo handles GET /api/file/{cid}/info.
|
||||
// Returns lightweight metadata for the file (existence, content type, size,
|
||||
// relative path) without streaming any bytes. Used by the editor to decide
|
||||
// whether to render a preview (image/video) or a download-only button.
|
||||
func (s *Server) handleGetFileByCIDInfo(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
cid := vars["cid"]
|
||||
|
||||
type infoResp struct {
|
||||
Exists bool `json:"exists"`
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
FilePath string `json:"filePath,omitempty"`
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if cid == "" {
|
||||
_ = json.NewEncoder(w).Encode(infoResp{Exists: false})
|
||||
return
|
||||
}
|
||||
if !s.storage.IsConnected() {
|
||||
writeError(w, http.StatusServiceUnavailable, "storage not connected")
|
||||
return
|
||||
}
|
||||
|
||||
relPath, err := s.storage.LookupPathByCID(cid)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if relPath == "" {
|
||||
_ = json.NewEncoder(w).Encode(infoResp{Exists: false})
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(s.config.FilesPath, relPath)
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
_ = json.NewEncoder(w).Encode(infoResp{Exists: false, FilePath: relPath})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(fullPath))
|
||||
contentType, ok := contentTypeByExt[ext]
|
||||
if !ok {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(infoResp{
|
||||
Exists: true,
|
||||
ContentType: contentType,
|
||||
Size: fi.Size(),
|
||||
FilePath: relPath,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetFileByCID handles GET /file/{cid}
|
||||
// Serves a file by looking up its CID in poster/backdrop metadata
|
||||
func (s *Server) handleGetFileByCID(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// handleSchemaGet returns the live per-field schema derived from meta:events.
|
||||
func (s *Server) handleSchemaGet(w http.ResponseWriter, r *http.Request) {
|
||||
if s.schemaIndexer == nil {
|
||||
http.Error(w, "schema indexer not running", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
snap := s.schemaIndexer.Snapshot()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(snap)
|
||||
}
|
||||
|
||||
// handleSchemaRescan re-emits a set event for every existing property via the
|
||||
// meta publisher, waits for the indexer to drain, and returns the refreshed
|
||||
// schema. Equivalent to a full SCAN-based rebuild without locking Redis.
|
||||
func (s *Server) handleSchemaRescan(w http.ResponseWriter, r *http.Request) {
|
||||
if s.schemaIndexer == nil {
|
||||
http.Error(w, "schema indexer not running", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := s.RepublishMetadata()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
s.schemaIndexer.WaitForDrain(10 * time.Second)
|
||||
|
||||
snap := s.schemaIndexer.Snapshot()
|
||||
snap.Source = "rescan"
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"published_events": count,
|
||||
"schema": snap,
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/metazla/meta-core/internal/events"
|
||||
"github.com/metazla/meta-core/internal/leader"
|
||||
"github.com/metazla/meta-core/internal/mounts"
|
||||
"github.com/metazla/meta-core/internal/schema"
|
||||
"github.com/metazla/meta-core/internal/storage"
|
||||
"github.com/metazla/meta-core/internal/watcher"
|
||||
"github.com/metazla/meta-core/internal/watchers"
|
||||
@@ -37,6 +38,7 @@ type Server struct {
|
||||
watchersPoller *watchers.Poller
|
||||
watchersHandlers *watchers.Handlers
|
||||
metaPublisher *events.MetaPublisher
|
||||
schemaIndexer *schema.Indexer
|
||||
webdavHandler *webdav.Handler
|
||||
router *mux.Router
|
||||
server *http.Server
|
||||
@@ -163,6 +165,10 @@ func (s *Server) setupRoutes() {
|
||||
s.router.HandleFunc("/api/metadata/{hashId}", s.handleUpdateMetadataByHashId).Methods("PUT")
|
||||
s.router.HandleFunc("/api/metadata/{hashId}", s.handleDeleteMetadataByHashId).Methods("DELETE")
|
||||
|
||||
// Schema inference
|
||||
s.router.HandleFunc("/api/schema", s.handleSchemaGet).Methods("GET")
|
||||
s.router.HandleFunc("/api/schema/rescan", s.handleSchemaRescan).Methods("POST")
|
||||
|
||||
// KV Browser API routes
|
||||
s.router.HandleFunc("/api/kv/info", s.handleKVInfo).Methods("GET")
|
||||
s.router.HandleFunc("/api/kv/keys", s.handleKVKeys).Methods("GET")
|
||||
@@ -194,6 +200,7 @@ func (s *Server) setupRoutes() {
|
||||
s.router.HandleFunc("/data/{hash}", s.handleHeadData).Methods("HEAD")
|
||||
|
||||
// File operations (by CID)
|
||||
s.router.HandleFunc("/api/file/{cid}/info", s.handleGetFileByCIDInfo).Methods("GET")
|
||||
s.router.HandleFunc("/file/{cid}", s.handleGetFileByCID).Methods("GET")
|
||||
s.router.HandleFunc("/file/cid", s.handleComputeFileCID).Methods("POST")
|
||||
|
||||
@@ -285,6 +292,15 @@ func (s *Server) Start() error {
|
||||
if err := s.metaPublisher.Start(); err != nil {
|
||||
log.Printf("[API] Warning: failed to start meta publisher: %v", err)
|
||||
} else {
|
||||
// Schema indexer consumes the same stream to derive a live
|
||||
// per-field schema (type/hint/breakdown). Start it before the
|
||||
// republish flood so it captures the bootstrap events.
|
||||
s.schemaIndexer = schema.NewIndexer(s.storage.GetRedisClient(), s.storage.GetPrefix())
|
||||
if err := s.schemaIndexer.Start(); err != nil {
|
||||
log.Printf("[API] Warning: failed to start schema indexer: %v", err)
|
||||
s.schemaIndexer = nil
|
||||
}
|
||||
|
||||
// Republish events for all existing metadata
|
||||
go func() {
|
||||
count, err := s.RepublishMetadata()
|
||||
@@ -321,6 +337,13 @@ func (s *Server) Stop() error {
|
||||
s.mountStatsPoller.Stop()
|
||||
}
|
||||
|
||||
// Stop schema indexer before the publisher so the consumer drains cleanly
|
||||
if s.schemaIndexer != nil {
|
||||
if err := s.schemaIndexer.Stop(); err != nil {
|
||||
log.Printf("[API] Warning: failed to stop schema indexer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop meta publisher
|
||||
if s.metaPublisher != nil {
|
||||
if err := s.metaPublisher.Stop(); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user