131 lines
2.9 KiB
Go
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)
|
|
}
|