Files
ai-disclosure-standard/pkg/licenseserver/store.go
T
2026-07-20 21:03:05 +02:00

131 lines
2.9 KiB
Go

package licenseserver
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sort"
"sync"
"time"
)
type Record struct {
LicenseID string `json:"licenseId"`
TokenHash string `json:"tokenHash"`
Product string `json:"product"`
Customer string `json:"customer"`
Edition string `json:"edition"`
ExpiresAt int64 `json:"expiresAt"`
Revoked bool `json:"revoked"`
Reason string `json:"reason,omitempty"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
type fileDocument struct {
Version int `json:"version"`
Records []Record `json:"records"`
}
type Store struct {
mu sync.RWMutex
path string
records map[string]Record
}
func OpenStore(path string) (*Store, error) {
s := &Store{path: path, records: map[string]Record{}}
if path == "" {
return s, nil
}
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return s, nil
}
if err != nil {
return nil, err
}
var doc fileDocument
if err := json.Unmarshal(data, &doc); err != nil {
return nil, err
}
if doc.Version != 1 {
return nil, errors.New("unsupported license registry version")
}
for _, record := range doc.Records {
s.records[record.LicenseID] = record
}
return s, nil
}
func (s *Store) Get(id string) (Record, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
record, ok := s.records[id]
return record, ok
}
func (s *Store) List() []Record {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]Record, 0, len(s.records))
for _, record := range s.records {
out = append(out, record)
}
sort.Slice(out, func(i, j int) bool { return out[i].UpdatedAt > out[j].UpdatedAt })
return out
}
func (s *Store) Put(record Record) error {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UTC().Unix()
if existing, ok := s.records[record.LicenseID]; ok {
record.CreatedAt = existing.CreatedAt
}
if record.CreatedAt == 0 {
record.CreatedAt = now
}
record.UpdatedAt = now
s.records[record.LicenseID] = record
return s.persistLocked()
}
func (s *Store) SetRevoked(id string, revoked bool, reason string) error {
s.mu.Lock()
defer s.mu.Unlock()
record, ok := s.records[id]
if !ok {
return os.ErrNotExist
}
record.Revoked = revoked
record.Reason = reason
record.UpdatedAt = time.Now().UTC().Unix()
s.records[id] = record
return s.persistLocked()
}
func (s *Store) persistLocked() error {
if s.path == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil && filepath.Dir(s.path) != "." {
return err
}
records := make([]Record, 0, len(s.records))
for _, record := range s.records {
records = append(records, record)
}
sort.Slice(records, func(i, j int) bool { return records[i].LicenseID < records[j].LicenseID })
data, err := json.MarshalIndent(fileDocument{Version: 1, Records: records}, "", " ")
if err != nil {
return err
}
temp := s.path + ".tmp"
if err := os.WriteFile(temp, data, 0o600); err != nil {
return err
}
return os.Rename(temp, s.path)
}