cron.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package cron
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "log/slog"
  7. "sync"
  8. "time"
  9. cf "goflare/internal/cloudflare"
  10. "goflare/internal/database/queries"
  11. "goflare/internal/ipfetcher"
  12. "github.com/robfig/cron/v3"
  13. )
  14. const (
  15. settingKeyCurrentIP = "current_ip"
  16. settingKeyCurrentIPUpdatedAt = "current_ip_updated_at"
  17. )
  18. type DDNSUpdater struct {
  19. q *queries.Queries
  20. cron *cron.Cron
  21. entryID cron.EntryID
  22. schedule string
  23. mu sync.Mutex
  24. lastIP string
  25. }
  26. func NewDDNSUpdater(q *queries.Queries) *DDNSUpdater {
  27. return &DDNSUpdater{
  28. q: q,
  29. cron: cron.New(),
  30. }
  31. }
  32. func (u *DDNSUpdater) Start(schedule string) error {
  33. id, err := u.cron.AddFunc(schedule, u.run)
  34. if err != nil {
  35. return fmt.Errorf("invalid cron schedule %q: %w", schedule, err)
  36. }
  37. u.entryID = id
  38. u.schedule = schedule
  39. u.cron.Start()
  40. ctx := context.Background()
  41. cached, err := u.q.GetSetting(ctx, settingKeyCurrentIP)
  42. if err == nil && cached != "" {
  43. u.mu.Lock()
  44. u.lastIP = cached
  45. u.mu.Unlock()
  46. }
  47. if err != nil && err != sql.ErrNoRows {
  48. slog.Warn("failed to load current_ip setting", "error", err)
  49. }
  50. slog.Info("ddns updater started", "schedule", schedule)
  51. return nil
  52. }
  53. func (u *DDNSUpdater) Stop() {
  54. u.cron.Stop()
  55. slog.Info("ddns updater stopped")
  56. }
  57. func (u *DDNSUpdater) Schedule() string {
  58. u.mu.Lock()
  59. defer u.mu.Unlock()
  60. return u.schedule
  61. }
  62. func (u *DDNSUpdater) Reschedule(schedule string) error {
  63. u.mu.Lock()
  64. defer u.mu.Unlock()
  65. id, err := u.cron.AddFunc(schedule, u.run)
  66. if err != nil {
  67. return fmt.Errorf("invalid cron schedule %q: %w", schedule, err)
  68. }
  69. u.cron.Remove(u.entryID)
  70. u.entryID = id
  71. u.schedule = schedule
  72. slog.Info("ddns updater rescheduled", "schedule", schedule)
  73. return nil
  74. }
  75. func (u *DDNSUpdater) run() {
  76. ctx := context.Background()
  77. urls, err := u.q.ListEnabledIpProviderUrls(ctx)
  78. if err != nil {
  79. slog.Error("failed to load IP providers", "error", err)
  80. return
  81. }
  82. var urlsOrNil []string
  83. if len(urls) > 0 {
  84. urlsOrNil = urls
  85. }
  86. ip, err := ipfetcher.FetchPublicIPFrom(ctx, urlsOrNil)
  87. if err != nil {
  88. slog.Error("failed to fetch public IP", "error", err)
  89. return
  90. }
  91. u.mu.Lock()
  92. lastIP := u.lastIP
  93. u.mu.Unlock()
  94. if ip == lastIP {
  95. slog.Debug("public IP unchanged", "ip", ip)
  96. return
  97. }
  98. slog.Info("public IP changed", "old", lastIP, "new", ip)
  99. records, err := u.q.ListNonStaticRecords(ctx)
  100. if err != nil {
  101. slog.Error("failed to list non-static records", "error", err)
  102. return
  103. }
  104. if len(records) == 0 {
  105. if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIP, Value: ip}); err != nil {
  106. slog.Error("failed to persist current_ip", "error", err)
  107. } else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: time.Now().UTC().Format(time.RFC3339)}); err != nil {
  108. slog.Error("failed to persist current_ip_updated_at", "error", err)
  109. }
  110. u.mu.Lock()
  111. u.lastIP = ip
  112. u.mu.Unlock()
  113. return
  114. }
  115. allOK := true
  116. for _, rec := range records {
  117. err := cf.UpdateDNSRecord(ctx, rec.ZoneApiKey, rec.CfZoneID, rec.CfRecordID, cf.DNSRecord{
  118. Type: rec.Type,
  119. Name: rec.Name,
  120. Content: ip,
  121. Proxied: rec.Proxied == 1,
  122. TTL: 1, // auto
  123. })
  124. if err != nil {
  125. slog.Error("failed to update DNS record",
  126. "record", rec.Name,
  127. "zone", rec.CfZoneID,
  128. "error", err,
  129. )
  130. allOK = false
  131. continue
  132. }
  133. err = u.q.UpdateRecordContent(ctx, queries.UpdateRecordContentParams{
  134. ID: rec.ID,
  135. Content: ip,
  136. })
  137. if err != nil {
  138. slog.Error("failed to update local record content",
  139. "record", rec.Name,
  140. "error", err,
  141. )
  142. allOK = false
  143. continue
  144. }
  145. slog.Info("updated DNS record", "record", rec.Name, "ip", ip)
  146. }
  147. if allOK {
  148. if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIP, Value: ip}); err != nil {
  149. slog.Error("failed to persist current_ip", "error", err)
  150. } else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: time.Now().UTC().Format(time.RFC3339)}); err != nil {
  151. slog.Error("failed to persist current_ip_updated_at", "error", err)
  152. }
  153. u.mu.Lock()
  154. u.lastIP = ip
  155. u.mu.Unlock()
  156. }
  157. }