mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 15:05:00 +00:00
233 lines
5.0 KiB
Go
233 lines
5.0 KiB
Go
package iplist
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/therootcompany/golib/sync/cachable"
|
|
)
|
|
|
|
const (
|
|
DefaultRefreshInterval = time.Hour + 57*time.Minute + 13*time.Second
|
|
DefaultRefreshTimeout = 15 * time.Minute
|
|
)
|
|
|
|
type IPListConfig struct {
|
|
Source string
|
|
CacheDir string
|
|
HTTPClient *http.Client
|
|
RefreshInterval time.Duration
|
|
Optional bool
|
|
RefreshTimeout time.Duration
|
|
}
|
|
|
|
type IPList struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
config IPListConfig
|
|
current atomic.Pointer[[]string]
|
|
start sync.Once
|
|
generation atomic.Uint64
|
|
loadedAt atomic.Pointer[time.Time]
|
|
lastErr atomic.Pointer[error]
|
|
refresh cachable.Refresh
|
|
}
|
|
|
|
var (
|
|
_ cachable.Cacheable[[]string] = (*IPList)(nil)
|
|
_ cachable.Mutable[[]string] = (*IPList)(nil)
|
|
_ cachable.Inspectable = (*IPList)(nil)
|
|
_ cachable.Tickable = (*IPList)(nil)
|
|
)
|
|
|
|
func NewIPList(ctx context.Context, config IPListConfig) (*IPList, error) {
|
|
if config.RefreshInterval <= 0 {
|
|
config.RefreshInterval = DefaultRefreshInterval
|
|
}
|
|
sourceCtx, cancel := context.WithCancel(ctx)
|
|
s := &IPList{ctx: sourceCtx, cancel: cancel, config: config}
|
|
if _, err := s.Load(ctx, true); err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (s *IPList) Current() *[]string {
|
|
return s.current.Load()
|
|
}
|
|
|
|
func (s *IPList) Due(ctx context.Context) (bool, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return false, err
|
|
}
|
|
status := s.Status()
|
|
return !status.HasValue || status.StaleAt.IsZero() || !time.Now().Before(status.StaleAt), nil
|
|
}
|
|
|
|
func (s *IPList) Revalidate(ctx context.Context) (started bool, err error) {
|
|
due, err := s.Due(ctx)
|
|
if err != nil || !due {
|
|
return false, err
|
|
}
|
|
if !s.refresh.Begin() {
|
|
return false, nil
|
|
}
|
|
go func() {
|
|
timeout := s.config.RefreshTimeout
|
|
if timeout <= 0 {
|
|
timeout = DefaultRefreshTimeout
|
|
}
|
|
refreshCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), timeout)
|
|
defer cancel()
|
|
s.refresh.Finish(s.update(refreshCtx))
|
|
}()
|
|
return true, nil
|
|
}
|
|
|
|
func (s *IPList) update(ctx context.Context) error {
|
|
generation := s.generation.Load()
|
|
entries, err := s.load(ctx)
|
|
if err != nil {
|
|
errCopy := err
|
|
s.lastErr.Store(&errCopy)
|
|
return err
|
|
}
|
|
old := s.current.Load()
|
|
changed := old == nil || !sameEntries(*old, entries)
|
|
if changed && s.generation.Load() == generation {
|
|
s.store(entries)
|
|
}
|
|
s.lastErr.Store(nil)
|
|
return nil
|
|
}
|
|
|
|
func (s *IPList) Load(ctx context.Context, wait bool) (*[]string, error) {
|
|
_, startErr := s.Revalidate(ctx)
|
|
if startErr != nil {
|
|
return s.Current(), startErr
|
|
}
|
|
if s.Current() == nil || wait {
|
|
if err := s.refresh.Wait(ctx); err != nil {
|
|
return s.Current(), err
|
|
}
|
|
}
|
|
return s.Current(), nil
|
|
}
|
|
|
|
func (s *IPList) Set(entries *[]string) error {
|
|
if entries == nil {
|
|
return s.Clear()
|
|
}
|
|
s.generation.Add(1)
|
|
s.store(*entries)
|
|
return nil
|
|
}
|
|
|
|
func (s *IPList) Clear() error {
|
|
s.generation.Add(1)
|
|
s.current.Store(nil)
|
|
s.loadedAt.Store(nil)
|
|
return nil
|
|
}
|
|
|
|
func (s *IPList) Status() cachable.Status {
|
|
loadedAt := time.Time{}
|
|
if t := s.loadedAt.Load(); t != nil {
|
|
loadedAt = *t
|
|
}
|
|
var lastErr error
|
|
if p := s.lastErr.Load(); p != nil {
|
|
lastErr = *p
|
|
}
|
|
status := cachable.Status{LoadedAt: loadedAt, HasValue: s.Current() != nil, Refreshing: s.refresh.Running(), LastError: lastErr}
|
|
if !loadedAt.IsZero() {
|
|
status.StaleAt = loadedAt.Add(s.config.RefreshInterval)
|
|
}
|
|
return status
|
|
}
|
|
|
|
func (s *IPList) Start(ctx context.Context, interval time.Duration) {
|
|
s.start.Do(func() {
|
|
if interval <= 0 {
|
|
interval = s.config.RefreshInterval
|
|
}
|
|
go func() {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-s.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
_, _ = s.Revalidate(s.ctx)
|
|
}
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
|
|
func (s *IPList) Stop() error {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
if s.cancel != nil {
|
|
s.cancel()
|
|
}
|
|
timeout := s.config.RefreshTimeout
|
|
if timeout <= 0 {
|
|
timeout = DefaultRefreshTimeout
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
return s.refresh.Stop(ctx)
|
|
}
|
|
|
|
func (s *IPList) load(ctx context.Context) ([]string, error) {
|
|
if s.config.Source == "" {
|
|
if s.config.Optional {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New("source is required")
|
|
}
|
|
if s.config.Optional {
|
|
parsed, parseErr := url.Parse(s.config.Source)
|
|
isURL := parseErr == nil && (parsed.Scheme == "http" || parsed.Scheme == "https")
|
|
if !isURL {
|
|
if _, err := os.Stat(s.config.Source); os.IsNotExist(err) {
|
|
return nil, nil
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
return Load(ctx, s.config.Source, s.config.CacheDir, s.config.HTTPClient)
|
|
}
|
|
|
|
func (s *IPList) store(entries []string) {
|
|
cp := append([]string(nil), entries...)
|
|
s.current.Store(&cp)
|
|
now := time.Now()
|
|
s.loadedAt.Store(&now)
|
|
}
|
|
|
|
func sameEntries(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|