180 lines
4.7 KiB
Go
180 lines
4.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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()
|
|
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})
|
|
}
|