feat: added record qso dvk
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package clublog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ctyURL is the ClubLog Country File endpoint. It returns a gzipped cty.xml.
|
||||
const ctyURL = "https://cdn.clublog.org/cty.php?api="
|
||||
|
||||
// Manager owns the on-disk cty.xml.gz cache and the parsed exception DB.
|
||||
type Manager struct {
|
||||
apiKey string
|
||||
cacheDir string
|
||||
|
||||
mu sync.RWMutex
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewManager(apiKey, cacheDir string) *Manager {
|
||||
return &Manager{apiKey: apiKey, cacheDir: cacheDir}
|
||||
}
|
||||
|
||||
// Path is where the cached gzipped country file lives.
|
||||
func (m *Manager) Path() string {
|
||||
return filepath.Join(m.cacheDir, "clublog_cty.xml.gz")
|
||||
}
|
||||
|
||||
// Loaded reports whether an exception DB is in memory.
|
||||
func (m *Manager) Loaded() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.db != nil
|
||||
}
|
||||
|
||||
// Info returns the loaded file's generation date + exception count (zeros when
|
||||
// not loaded).
|
||||
func (m *Manager) Info() (date string, count int) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if m.db == nil {
|
||||
return "", 0
|
||||
}
|
||||
return m.db.Date(), m.db.Count()
|
||||
}
|
||||
|
||||
// EnsureLoaded loads the cached file into memory if present. Does NOT download.
|
||||
func (m *Manager) EnsureLoaded() error {
|
||||
if m.Loaded() {
|
||||
return nil
|
||||
}
|
||||
return m.LoadFromDisk()
|
||||
}
|
||||
|
||||
// LoadFromDisk parses the cached cty.xml.gz and swaps it in.
|
||||
func (m *Manager) LoadFromDisk() error {
|
||||
f, err := os.Open(m.Path())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
db, err := LoadGzip(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.db = db
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Download fetches a fresh cty.xml.gz from ClubLog and writes it atomically,
|
||||
// then loads it.
|
||||
func (m *Manager) Download(ctx context.Context) error {
|
||||
if m.apiKey == "" {
|
||||
return fmt.Errorf("clublog api key not set")
|
||||
}
|
||||
if err := os.MkdirAll(m.cacheDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ctyURL+m.apiKey, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("clublog HTTP %d", resp.StatusCode)
|
||||
}
|
||||
tmp, err := os.CreateTemp(m.cacheDir, "clublog-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
if _, err := io.Copy(tmp, resp.Body); err != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmpName)
|
||||
return err
|
||||
}
|
||||
tmp.Close()
|
||||
if err := os.Rename(tmpName, m.Path()); err != nil {
|
||||
os.Remove(tmpName)
|
||||
return err
|
||||
}
|
||||
return m.LoadFromDisk()
|
||||
}
|
||||
|
||||
// Resolve returns the matching exception for a callsign at a date, if loaded.
|
||||
func (m *Manager) Resolve(call string, date time.Time) (Exception, bool) {
|
||||
m.mu.RLock()
|
||||
db := m.db
|
||||
m.mu.RUnlock()
|
||||
if db == nil {
|
||||
return Exception{}, false
|
||||
}
|
||||
return db.Resolve(call, date)
|
||||
}
|
||||
Reference in New Issue
Block a user