improve editor and add type detection

This commit is contained in:
Pierre
2026-05-14 23:31:07 +02:00
parent 3d65e7e951
commit aa45f4f32e
37 changed files with 2309 additions and 4220 deletions
+57
View File
@@ -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) {
+45
View File
@@ -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,
})
}
+23
View File
@@ -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 {