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 }