- package.sh: 打包包含 configs 种子数据、tools 构建工具、deps 离线依赖、scripts 辅助脚本 - ImportSeedAssets: 首次启动检测空库自动导入 templates/profiles/overlays - face_gallery.html: 添加构建人脸库按钮 - device.html: 无标准配置时显示设备实际模型/资源,无人脸库配置时隐藏按钮 - register-face-gallery.py: 手动注册已构建人脸库到 standard_resources - deployment.md: 更新部署流程反映新包结构 - .gitignore: 排除 *.tar.gz 构建产物
267 lines
9.1 KiB
Go
267 lines
9.1 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"safesight-control/internal/storage"
|
|
)
|
|
|
|
// ImportSeedAssets imports all config assets (templates, profiles, overlays) from a
|
|
// seed configs directory on first run. Returns total import count.
|
|
// The seed directory is expected to have subdirectories: templates/, profiles/, overlays/.
|
|
func ImportSeedAssets(repo *storage.AssetsRepo, seedDir string) (int, error) {
|
|
if repo == nil {
|
|
return 0, nil
|
|
}
|
|
seedDir = filepath.Clean(strings.TrimSpace(seedDir))
|
|
if seedDir == "" {
|
|
return 0, nil
|
|
}
|
|
if _, err := os.Stat(seedDir); os.IsNotExist(err) {
|
|
return 0, nil
|
|
}
|
|
|
|
// Check if DB already has assets (not first run).
|
|
existing, err := repo.ListTemplates()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(existing) > 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
total := 0
|
|
for _, kind := range []string{"templates", "profiles", "overlays"} {
|
|
dir := filepath.Join(seedDir, kind)
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return total, err
|
|
}
|
|
for _, f := range files {
|
|
if f.IsDir() || !strings.HasSuffix(strings.ToLower(f.Name()), ".json") {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, f.Name())
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return total, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
var raw map[string]any
|
|
if err := json.Unmarshal(body, &raw); err != nil {
|
|
return total, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
if raw == nil {
|
|
raw = map[string]any{}
|
|
}
|
|
name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))))
|
|
if name == "" {
|
|
log.Printf("skip %s: empty name", path)
|
|
continue
|
|
}
|
|
desc := stringValue(raw["description"])
|
|
|
|
switch kind {
|
|
case "templates":
|
|
if strings.TrimSpace(stringValue(raw["source"])) == "" {
|
|
raw["source"] = "standard"
|
|
}
|
|
b, _ := marshalConfigJSON(raw)
|
|
if err := repo.SaveTemplate(name, desc, string(b)); err != nil {
|
|
return total, fmt.Errorf("template %s: %w", name, err)
|
|
}
|
|
case "profiles":
|
|
tmplName := stringValue(raw["primary_template_name"])
|
|
bizName := stringValue(raw["business_name"])
|
|
b, _ := marshalConfigJSON(raw)
|
|
if err := repo.SaveProfile(name, tmplName, bizName, desc, string(b)); err != nil {
|
|
return total, fmt.Errorf("profile %s: %w", name, err)
|
|
}
|
|
case "overlays":
|
|
b, _ := marshalConfigJSON(raw)
|
|
if err := repo.SaveOverlay(name, desc, string(b)); err != nil {
|
|
return total, fmt.Errorf("overlay %s: %w", name, err)
|
|
}
|
|
}
|
|
total++
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func ImportStandardTemplatesFromDir(repo *storage.AssetsRepo, dir string) (int, error) {
|
|
if repo == nil {
|
|
return 0, fmt.Errorf("asset repository is not configured")
|
|
}
|
|
dir = filepath.Clean(strings.TrimSpace(dir))
|
|
if dir == "" {
|
|
return 0, fmt.Errorf("standard template dir is empty")
|
|
}
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
imported := 0
|
|
for _, file := range files {
|
|
if file.IsDir() || strings.ToLower(filepath.Ext(file.Name())) != ".json" {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, file.Name())
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return imported, err
|
|
}
|
|
var raw map[string]any
|
|
if err := json.Unmarshal(body, &raw); err != nil {
|
|
return imported, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
if raw == nil {
|
|
raw = map[string]any{}
|
|
}
|
|
name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))))
|
|
if name == "" {
|
|
return imported, fmt.Errorf("%s: template name is empty", path)
|
|
}
|
|
if err := validateConfigName(name); err != nil {
|
|
return imported, fmt.Errorf("%s: invalid template name %q: %w", path, name, err)
|
|
}
|
|
if !isStandardTemplateName(name) {
|
|
return imported, fmt.Errorf("%s: standard template name must start with std_", path)
|
|
}
|
|
if strings.TrimSpace(stringValue(raw["source"])) == "" {
|
|
raw["source"] = "standard"
|
|
}
|
|
body, err = marshalConfigJSON(raw)
|
|
if err != nil {
|
|
return imported, err
|
|
}
|
|
existing, err := repo.GetTemplate(name)
|
|
if err != nil {
|
|
return imported, err
|
|
}
|
|
if existing != nil &&
|
|
strings.TrimSpace(existing.Description) == strings.TrimSpace(stringValue(raw["description"])) &&
|
|
strings.TrimSpace(existing.BodyJSON) == strings.TrimSpace(string(body)) {
|
|
continue
|
|
}
|
|
if err := repo.SaveTemplate(name, stringValue(raw["description"]), string(body)); err != nil {
|
|
return imported, err
|
|
}
|
|
imported++
|
|
}
|
|
return imported, nil
|
|
}
|
|
|
|
func ImportStandardOverlaysFromDir(repo *storage.AssetsRepo, dir string) (int, error) {
|
|
if repo == nil {
|
|
return 0, fmt.Errorf("asset repository is not configured")
|
|
}
|
|
dir = filepath.Clean(strings.TrimSpace(dir))
|
|
if dir == "" {
|
|
return 0, fmt.Errorf("standard overlay dir is empty")
|
|
}
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
imported := 0
|
|
for _, file := range files {
|
|
if file.IsDir() || strings.ToLower(filepath.Ext(file.Name())) != ".json" {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, file.Name())
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return imported, err
|
|
}
|
|
var raw map[string]any
|
|
if err := json.Unmarshal(body, &raw); err != nil {
|
|
return imported, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
if raw == nil {
|
|
raw = map[string]any{}
|
|
}
|
|
name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))))
|
|
if name == "" {
|
|
return imported, fmt.Errorf("%s: overlay name is empty", path)
|
|
}
|
|
if err := validateConfigName(name); err != nil {
|
|
return imported, fmt.Errorf("%s: invalid overlay name %q: %w", path, name, err)
|
|
}
|
|
if !isStandardTemplateName(name) {
|
|
return imported, fmt.Errorf("%s: standard overlay name must start with std_", path)
|
|
}
|
|
description := strings.TrimSpace(stringValue(raw["description"]))
|
|
delete(raw, "name")
|
|
delete(raw, "description")
|
|
body, err = marshalConfigJSON(raw)
|
|
if err != nil {
|
|
return imported, err
|
|
}
|
|
existing, err := repo.GetOverlay(name)
|
|
if err != nil {
|
|
return imported, err
|
|
}
|
|
if existing != nil &&
|
|
strings.TrimSpace(existing.Description) == description &&
|
|
strings.TrimSpace(existing.BodyJSON) == strings.TrimSpace(string(body)) {
|
|
continue
|
|
}
|
|
if err := repo.SaveOverlay(name, description, string(body)); err != nil {
|
|
return imported, err
|
|
}
|
|
imported++
|
|
}
|
|
return imported, nil
|
|
}
|
|
|
|
// EnsureDefaultIntegrations creates the default alarm and storage integration
|
|
// services if they don't already exist. Call during startup.
|
|
func EnsureDefaultIntegrations(repo *storage.AssetsRepo) error {
|
|
defaults := []struct {
|
|
Name string
|
|
ServiceType string
|
|
Description string
|
|
Enabled bool
|
|
BodyJSON string
|
|
}{
|
|
{
|
|
Name: "alarm",
|
|
ServiceType: "alarm_service",
|
|
Description: "告警推送 HTTP API",
|
|
Enabled: true,
|
|
BodyJSON: `{"get_token_url":"http://10.0.0.49:8080/api/getToken","put_message_url":"http://10.0.0.49:8080/api/putMessage","tenant_code":"32"}`,
|
|
},
|
|
{
|
|
Name: "storage",
|
|
ServiceType: "object_storage",
|
|
Description: "告警截图和视频存储",
|
|
Enabled: true,
|
|
BodyJSON: `{"endpoint":"http://10.0.0.49:9000","bucket":"myminio","access_key":"admin","secret_key":"password"}`,
|
|
},
|
|
}
|
|
for _, item := range defaults {
|
|
existing, _ := repo.GetIntegrationService(item.Name)
|
|
if existing != nil {
|
|
continue
|
|
}
|
|
if err := repo.SaveIntegrationService(item.Name, item.ServiceType, item.Description, item.Enabled, item.BodyJSON); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|