package api import ( "io" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/prism/proxy/internal/ruleio" ) func (s *Server) exportRules(c *gin.Context) { ctx := c.Request.Context() items, err := s.app.Store.ListRules(ctx, "manual") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } format := strings.ToLower(c.DefaultQuery("format", "json")) filename := "prism-rules" switch format { case "text", "txt", "clash": c.Header("Content-Type", "text/plain; charset=utf-8") c.Header("Content-Disposition", `attachment; filename="`+filename+`.txt"`) c.String(http.StatusOK, ruleio.ExportText(items)) default: data, err := ruleio.ExportJSON(items) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.Header("Content-Type", "application/json; charset=utf-8") c.Header("Content-Disposition", `attachment; filename="`+filename+`.json"`) c.Data(http.StatusOK, "application/json; charset=utf-8", data) } } type importRulesReq struct { Mode string `json:"mode"` Text string `json:"text"` Rules []ruleio.ExportRule `json:"rules"` } func (s *Server) importRules(c *gin.Context) { ctx := c.Request.Context() mode := strings.ToLower(strings.TrimSpace(c.DefaultQuery("mode", ""))) var parsed []ruleio.ExportRule var parseErrors []string contentType := c.GetHeader("Content-Type") if strings.Contains(contentType, "application/json") { var req importRulesReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if mode == "" { mode = strings.ToLower(strings.TrimSpace(req.Mode)) } if len(req.Rules) > 0 { parsed = req.Rules } else if strings.TrimSpace(req.Text) != "" { parsed, parseErrors = parseImportText(req.Text) } else { c.JSON(http.StatusBadRequest, gin.H{"error": "rules or text required"}) return } } else { body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if mode == "" { mode = "append" } parsed, parseErrors = parseImportText(string(body)) } if mode == "" { mode = "append" } if mode != "append" && mode != "replace" { c.JSON(http.StatusBadRequest, gin.H{"error": "mode must be append or replace"}) return } if len(parsed) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no valid rules", "errors": parseErrors}) return } rules, valErrors := ruleio.ToStoreRules(parsed) allErrors := append(parseErrors, valErrors...) if len(rules) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no valid rules", "errors": allErrors}) return } if mode == "append" { start, _ := s.app.Store.NextManualRulePriority(ctx) ruleio.AssignPriorities(rules, start) if err := s.app.Store.AppendManualRules(ctx, rules); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } else { ruleio.AssignPriorities(rules, 100) if err := s.app.Store.ReplaceManualRules(ctx, rules); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } _ = s.app.Store.AddAuditLog(ctx, "import", "rules", mode+": "+strconv.Itoa(len(rules))) _ = s.app.ReloadEngine(ctx) c.JSON(http.StatusOK, gin.H{ "ok": true, "imported": len(rules), "skipped": len(allErrors), "errors": allErrors, "mode": mode, }) } func parseImportText(text string) ([]ruleio.ExportRule, []string) { text = strings.TrimSpace(text) if text == "" { return nil, nil } if strings.HasPrefix(text, "{") || strings.HasPrefix(text, "[") { rules, err := ruleio.ParseJSON([]byte(text)) if err == nil && len(rules) > 0 { return rules, nil } } return ruleio.ParseText(text) }