| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- package cron
- import (
- "context"
- "database/sql"
- "fmt"
- "log/slog"
- "sync"
- "time"
- cf "goflare/internal/cloudflare"
- "goflare/internal/database/queries"
- "goflare/internal/ipfetcher"
- "github.com/robfig/cron/v3"
- )
- const (
- settingKeyCurrentIP = "current_ip"
- settingKeyCurrentIPUpdatedAt = "current_ip_updated_at"
- )
- type DDNSUpdater struct {
- q *queries.Queries
- cron *cron.Cron
- entryID cron.EntryID
- schedule string
- mu sync.Mutex
- lastIP string
- }
- func NewDDNSUpdater(q *queries.Queries) *DDNSUpdater {
- return &DDNSUpdater{
- q: q,
- cron: cron.New(),
- }
- }
- func (u *DDNSUpdater) Start(schedule string) error {
- id, err := u.cron.AddFunc(schedule, u.run)
- if err != nil {
- return fmt.Errorf("invalid cron schedule %q: %w", schedule, err)
- }
- u.entryID = id
- u.schedule = schedule
- u.cron.Start()
- ctx := context.Background()
- cached, err := u.q.GetSetting(ctx, settingKeyCurrentIP)
- if err == nil && cached != "" {
- u.mu.Lock()
- u.lastIP = cached
- u.mu.Unlock()
- }
- if err != nil && err != sql.ErrNoRows {
- slog.Warn("failed to load current_ip setting", "error", err)
- }
- slog.Info("ddns updater started", "schedule", schedule)
- return nil
- }
- func (u *DDNSUpdater) Stop() {
- u.cron.Stop()
- slog.Info("ddns updater stopped")
- }
- func (u *DDNSUpdater) Schedule() string {
- u.mu.Lock()
- defer u.mu.Unlock()
- return u.schedule
- }
- func (u *DDNSUpdater) Reschedule(schedule string) error {
- u.mu.Lock()
- defer u.mu.Unlock()
- id, err := u.cron.AddFunc(schedule, u.run)
- if err != nil {
- return fmt.Errorf("invalid cron schedule %q: %w", schedule, err)
- }
- u.cron.Remove(u.entryID)
- u.entryID = id
- u.schedule = schedule
- slog.Info("ddns updater rescheduled", "schedule", schedule)
- return nil
- }
- func (u *DDNSUpdater) run() {
- ctx := context.Background()
- urls, err := u.q.ListEnabledIpProviderUrls(ctx)
- if err != nil {
- slog.Error("failed to load IP providers", "error", err)
- return
- }
- var urlsOrNil []string
- if len(urls) > 0 {
- urlsOrNil = urls
- }
- ip, err := ipfetcher.FetchPublicIPFrom(ctx, urlsOrNil)
- if err != nil {
- slog.Error("failed to fetch public IP", "error", err)
- return
- }
- u.mu.Lock()
- lastIP := u.lastIP
- u.mu.Unlock()
- if ip == lastIP {
- slog.Debug("public IP unchanged", "ip", ip)
- return
- }
- slog.Info("public IP changed", "old", lastIP, "new", ip)
- records, err := u.q.ListNonStaticRecords(ctx)
- if err != nil {
- slog.Error("failed to list non-static records", "error", err)
- return
- }
- if len(records) == 0 {
- if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIP, Value: ip}); err != nil {
- slog.Error("failed to persist current_ip", "error", err)
- } else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: time.Now().UTC().Format(time.RFC3339)}); err != nil {
- slog.Error("failed to persist current_ip_updated_at", "error", err)
- }
- u.mu.Lock()
- u.lastIP = ip
- u.mu.Unlock()
- return
- }
- allOK := true
- for _, rec := range records {
- err := cf.UpdateDNSRecord(ctx, rec.ZoneApiKey, rec.CfZoneID, rec.CfRecordID, cf.DNSRecord{
- Type: rec.Type,
- Name: rec.Name,
- Content: ip,
- Proxied: rec.Proxied == 1,
- TTL: 1, // auto
- })
- if err != nil {
- slog.Error("failed to update DNS record",
- "record", rec.Name,
- "zone", rec.CfZoneID,
- "error", err,
- )
- allOK = false
- continue
- }
- err = u.q.UpdateRecordContent(ctx, queries.UpdateRecordContentParams{
- ID: rec.ID,
- Content: ip,
- })
- if err != nil {
- slog.Error("failed to update local record content",
- "record", rec.Name,
- "error", err,
- )
- allOK = false
- continue
- }
- slog.Info("updated DNS record", "record", rec.Name, "ip", ip)
- }
- if allOK {
- if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIP, Value: ip}); err != nil {
- slog.Error("failed to persist current_ip", "error", err)
- } else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: time.Now().UTC().Format(time.RFC3339)}); err != nil {
- slog.Error("failed to persist current_ip_updated_at", "error", err)
- }
- u.mu.Lock()
- u.lastIP = ip
- u.mu.Unlock()
- }
- }
|