package app import ( "encoding/json" "fmt" "path/filepath" "strings" "time" transportcfgpkg "selective-vpn-api/app/transportcfg" ) func appendSingBoxHistory(rec singBoxProfileHistoryRecord) error { profileID := sanitizeID(rec.ProfileID) if profileID == "" { return fmt.Errorf("empty profile id") } rec.ProfileID = profileID if strings.TrimSpace(rec.At) == "" { rec.At = time.Now().UTC().Format(time.RFC3339Nano) } if strings.TrimSpace(rec.ID) == "" { rec.ID = "h-" + newTransportToken(8) } if rec.Action == "" { rec.Action = "unknown" } if rec.Status == "" { rec.Status = "success" } data, err := json.MarshalIndent(rec, "", " ") if err != nil { return err } stamp := transportcfgpkg.SanitizeHistoryStamp(rec.At, time.Now().UTC()) fileName := fmt.Sprintf("%s-%s-%s-%s.json", stamp, rec.ProfileID, rec.Action, rec.ID) return transportcfgpkg.WriteFileAtomic(filepath.Join(singBoxHistoryRootDir, fileName), append(data, '\n'), 0o644) } func loadSingBoxHistoryRecords(profileID string, limit int) ([]singBoxProfileHistoryRecord, error) { rawRecords, err := transportcfgpkg.ReadJSONFiles(singBoxHistoryRootDir) if err != nil { return nil, err } id := sanitizeID(profileID) out := make([]singBoxProfileHistoryRecord, 0, len(rawRecords)) for _, data := range rawRecords { var rec singBoxProfileHistoryRecord if err := json.Unmarshal(data, &rec); err != nil { continue } if sanitizeID(rec.ProfileID) != id { continue } out = append(out, rec) } transportcfgpkg.SortRecordsDescByAt(out, func(rec singBoxProfileHistoryRecord) string { return rec.At }) if limit > 0 && len(out) > limit { out = out[:limit] } return out, nil } func selectSingBoxRollbackCandidate(records []singBoxProfileHistoryRecord, historyID string) (singBoxProfileHistoryRecord, bool) { return transportcfgpkg.SelectRecordCandidate( records, historyID, func(rec singBoxProfileHistoryRecord) string { return rec.ID }, func(rec singBoxProfileHistoryRecord) bool { return rec.Action == "apply" && rec.Status == "success" }, ) } func decodeHistoryPrevConfig(rec singBoxProfileHistoryRecord) ([]byte, bool, error) { return transportcfgpkg.DecodeBase64Optional(rec.PrevConfigB64, rec.PrevConfigExist) } func joinSingBoxIssueMessages(issues []SingBoxProfileIssue) string { parts := make([]string, 0, len(issues)) for _, it := range issues { parts = append(parts, it.Message) } return transportcfgpkg.JoinMessages(parts) }