package httpapi import ( "context" "encoding/json" "log" "net/http" "os" "path/filepath" "strings" "time" "safesight-agent/internal/files" ) type faceGalleryInfo struct { Ok bool `json:"ok"` Path string `json:"path"` Exists bool `json:"exists"` Size int64 `json:"size"` MtimeMS int64 `json:"mtime_ms"` } func (s *Server) faceGalleryPath() string { return filepath.Join(s.resourcesDir, "face_gallery", "face_gallery.db") } func (s *Server) handleFaceGallery(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } p := s.faceGalleryPath() st, err := os.Stat(p) if err != nil { if os.IsNotExist(err) { writeJSON(w, http.StatusOK, faceGalleryInfo{Ok: true, Path: p, Exists: false}) return } errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } writeJSON(w, http.StatusOK, faceGalleryInfo{Ok: true, Path: p, Exists: true, Size: st.Size(), MtimeMS: st.ModTime().UnixMilli()}) return case http.MethodPut: if !s.authorize(r, true) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } if ct := strings.TrimSpace(r.Header.Get("Content-Type")); ct != "application/octet-stream" { errorJSON(w, http.StatusBadRequest, "validation failed: Content-Type must be application/octet-stream") return } maxBytes := int64(s.agentCfg.MaxUploadMB) * 1024 * 1024 r.Body = http.MaxBytesReader(w, r.Body, maxBytes) dst := s.faceGalleryPath() dir := filepath.Dir(dst) if err := files.EnsureDir(dir, 0o755); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } f, err := os.CreateTemp(dir, ".tmp-face-gallery-*") if err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } tmp := f.Name() ok := false defer func() { _ = f.Close() if !ok { _ = os.Remove(tmp) } }() if _, err := f.ReadFrom(r.Body); err != nil { errorJSON(w, http.StatusBadRequest, "invalid body: "+err.Error()) return } if err := f.Chmod(0o644); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } if err := f.Sync(); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } if err := f.Close(); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } if err := files.ReplaceFile(tmp, dst); err != nil { s.recordAudit(r, "face_gallery.update", false, err.Error()) errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } ok = true st, err := os.Stat(dst) if err != nil { s.recordAudit(r, "face_gallery.update", false, err.Error()) errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } s.recordAudit(r, "face_gallery.update", true, "") writeJSON(w, http.StatusOK, faceGalleryInfo{Ok: true, Path: dst, Exists: true, Size: st.Size(), MtimeMS: st.ModTime().UnixMilli()}) // Auto-reload: notify edge-server to pick up the new gallery go s.reloadFaceGallery() return default: errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } } func (s *Server) reloadFaceGallery() { reloadSeq := time.Now().UnixMilli() log.Printf("[face_gallery] auto-reload seq=%d", reloadSeq) reloaded := 0 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() st, b, err := s.ms.GetGraphs(ctx) if err != nil { return } if st < 200 || st > 299 { return } var graphs []struct { Name string `json:"name"` } if err := json.Unmarshal(b, &graphs); err != nil { return } for _, g := range graphs { name := strings.TrimSpace(g.Name) if name == "" { continue } st2, b2, err := s.ms.GetGraph(ctx, name) if err != nil || st2 < 200 || st2 > 299 { continue } var snap struct { Nodes []struct { ID string `json:"id"` Type string `json:"type"` } `json:"nodes"` } if err := json.Unmarshal(b2, &snap); err != nil { continue } for _, n := range snap.Nodes { if n.Type != "ai_face_recog" { continue } s.ms.UpdateNodeConfig(ctx, n.ID, name, map[string]any{"gallery": map[string]any{"reload_seq": reloadSeq}}) reloaded++ } } } func (s *Server) handleFaceGalleryReload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, true) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } s.reloadFaceGallery() s.recordAudit(r, "face_gallery.reload", true, "") writeJSON(w, http.StatusOK, map[string]any{"ok": true}) }