package httpapi import ( "bytes" "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" "time" "rk3588sys/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.agentCfg.ModelsDir, "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()}) return default: errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } } 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 } ctx := r.Context() st, b, err := s.ms.GetGraphs(ctx) if err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } if st < 200 || st > 299 { errorJSON(w, http.StatusInternalServerError, fmt.Sprintf("internal error: media-server status=%d body=%s", st, strings.TrimSpace(string(bytes.TrimSpace(b))))) return } var graphs []struct { Name string `json:"name"` } if err := json.Unmarshal(b, &graphs); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: parse graphs failed: "+err.Error()) return } reloadSeq := time.Now().UnixMilli() reloaded := 0 for _, g := range graphs { if strings.TrimSpace(g.Name) == "" { continue } st2, b2, err := s.ms.GetGraph(ctx, g.Name) if err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error()) return } if st2 < 200 || st2 > 299 { errorJSON(w, http.StatusInternalServerError, fmt.Sprintf("internal error: media-server status=%d body=%s", st2, strings.TrimSpace(string(bytes.TrimSpace(b2))))) return } var snap struct { Nodes []struct { ID string `json:"id"` Type string `json:"type"` } `json:"nodes"` } if err := json.Unmarshal(b2, &snap); err != nil { errorJSON(w, http.StatusInternalServerError, "internal error: parse graph snapshot failed: "+err.Error()) return } for _, n := range snap.Nodes { if n.Type != "ai_face_recog" { continue } patch := map[string]any{"gallery": map[string]any{"reload_seq": reloadSeq}} if err := s.ms.UpdateNodeConfig(ctx, n.ID, g.Name, patch); err != nil { s.recordAudit(r, "face_gallery.reload", false, err.Error()) errorJSON(w, http.StatusInternalServerError, "internal error: update node config failed: "+err.Error()) return } reloaded++ } } s.recordAudit(r, "face_gallery.reload", true, "") writeJSON(w, http.StatusOK, map[string]any{"ok": true, "reloaded": reloaded, "reload_seq": reloadSeq}) }