package main import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "regexp" "strings" "sync" ) // Tag colore un mot-clé dans les messages affichés. type Tag struct { ID string `json:"id"` Pattern string `json:"pattern"` Color string `json:"color"` WholeWord bool `json:"wholeWord"` CaseSensitive bool `json:"caseSensitive"` Regex bool `json:"regex"` Enabled bool `json:"enabled"` } func defaultTags() []Tag { return []Tag{ {ID: "warning", Pattern: "warning", Color: "#f59e0b", WholeWord: true, Enabled: true}, {ID: "error", Pattern: "error", Color: "#ef4444", WholeWord: true, Enabled: true}, {ID: "ok", Pattern: "ok", Color: "#22c55e", WholeWord: true, Enabled: true}, } } var ( errTagNotFound = errors.New("tag introuvable") colorRe = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) ) func (t *Tag) validate() error { t.Pattern = strings.TrimSpace(t.Pattern) if t.Pattern == "" { return errors.New("le mot-clé est obligatoire") } if !colorRe.MatchString(t.Color) { return errors.New("couleur invalide (format #rrggbb attendu)") } if t.Regex { if _, err := regexp.Compile(t.Pattern); err != nil { return fmt.Errorf("expression régulière invalide : %v", err) } } return nil } // TagStore conserve les tags dans un fichier JSON (volume /data). type TagStore struct { mu sync.Mutex path string tags []Tag } func LoadTagStore(path string) (*TagStore, error) { s := &TagStore{path: path} b, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { s.tags = defaultTags() return s, s.save() } if err != nil { return nil, err } if err := json.Unmarshal(b, &s.tags); err != nil { return nil, fmt.Errorf("%s : %w", path, err) } return s, nil } // save écrit le fichier de façon atomique ; l'appelant détient le verrou. func (s *TagStore) save() error { b, err := json.MarshalIndent(s.tags, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, s.path) } func (s *TagStore) List() []Tag { s.mu.Lock() defer s.mu.Unlock() return append([]Tag(nil), s.tags...) } func (s *TagStore) Create(t Tag) (Tag, error) { if err := t.validate(); err != nil { return t, err } t.ID = newID() s.mu.Lock() defer s.mu.Unlock() s.tags = append(s.tags, t) return t, s.save() } func (s *TagStore) Update(id string, t Tag) (Tag, error) { if err := t.validate(); err != nil { return t, err } t.ID = id s.mu.Lock() defer s.mu.Unlock() for i := range s.tags { if s.tags[i].ID == id { s.tags[i] = t return t, s.save() } } return t, errTagNotFound } func (s *TagStore) Delete(id string) error { s.mu.Lock() defer s.mu.Unlock() for i := range s.tags { if s.tags[i].ID == id { s.tags = append(s.tags[:i], s.tags[i+1:]...) return s.save() } } return errTagNotFound } func (s *TagStore) Reset() ([]Tag, error) { s.mu.Lock() defer s.mu.Unlock() s.tags = defaultTags() return append([]Tag(nil), s.tags...), s.save() } func newID() string { b := make([]byte, 6) _, _ = rand.Read(b) return hex.EncodeToString(b) }