135 lines
2.5 KiB
Go
135 lines
2.5 KiB
Go
package cache
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Entry struct {
|
|
Name string `json:"name"`
|
|
IP string `json:"ip"`
|
|
Kind string `json:"kind"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
CachedAt time.Time `json:"cached_at"`
|
|
}
|
|
|
|
type Cache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]Entry
|
|
path string
|
|
ttl time.Duration
|
|
}
|
|
|
|
type diskFormat struct {
|
|
Entries []Entry `json:"entries"`
|
|
}
|
|
|
|
func New(path string, ttlSeconds int) *Cache {
|
|
return &Cache{
|
|
entries: make(map[string]Entry),
|
|
path: path,
|
|
ttl: time.Duration(ttlSeconds) * time.Second,
|
|
}
|
|
}
|
|
|
|
func (c *Cache) Load() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
data, err := os.ReadFile(c.path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var df diskFormat
|
|
if err := json.Unmarshal(data, &df); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.entries = make(map[string]Entry, len(df.Entries))
|
|
for _, e := range df.Entries {
|
|
c.entries[e.Name] = e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) Save() error {
|
|
c.mu.RLock()
|
|
df := diskFormat{Entries: make([]Entry, 0, len(c.entries))}
|
|
for _, e := range c.entries {
|
|
df.Entries = append(df.Entries, e)
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := json.MarshalIndent(df, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(c.path, data, 0o644)
|
|
}
|
|
|
|
func (c *Cache) Upsert(e Entry) {
|
|
e.CachedAt = time.Now()
|
|
c.mu.Lock()
|
|
c.entries[e.Name] = e
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Search returns all entries whose name starts with prefix (case-insensitive).
|
|
// TTL is intentionally ignored — this is used for shell completion.
|
|
func (c *Cache) Search(prefix string) []Entry {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
prefix = strings.ToLower(prefix)
|
|
var out []Entry
|
|
for name, e := range c.entries {
|
|
if strings.HasPrefix(strings.ToLower(name), prefix) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Get returns an entry and reports whether it is still within the TTL.
|
|
func (c *Cache) Get(name string) (entry Entry, fresh bool) {
|
|
c.mu.RLock()
|
|
e, ok := c.entries[name]
|
|
c.mu.RUnlock()
|
|
|
|
if !ok {
|
|
return Entry{}, false
|
|
}
|
|
if c.ttl == 0 {
|
|
return e, false
|
|
}
|
|
return e, time.Since(e.CachedAt) < c.ttl
|
|
}
|
|
|
|
func (c *Cache) Clear() {
|
|
c.mu.Lock()
|
|
c.entries = make(map[string]Entry)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *Cache) All() []Entry {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
out := make([]Entry, 0, len(c.entries))
|
|
for _, e := range c.entries {
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|