cron.go 3.6 KB

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