Files
logstream/tags.go
T
2026-09-28 12:19:29 +02:00

152 lines
3.2 KiB
Go

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)
}