Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

graft-source / cmd / sync / main.go
3.3 kB 134 lines
1// Command sync runs the Forgejo <-> Radicle sync daemon: on an interval, it 2// mirrors git content, issues and patches for every repo pair listed in the 3// config file, then sleeps until the next pass. 4package main 5 6import ( 7 "flag" 8 "log/slog" 9 "net/http" 10 "net/url" 11 "os" 12 "strings" 13 "time" 14 15 "graft/internal/config" 16 "graft/internal/state" 17 "graft/internal/status" 18 "graft/internal/sync" 19) 20 21func main() { 22 configPath := flag.String("config", "/etc/graft/config.yaml", "path to config.yaml") 23 once := flag.Bool("once", false, "run a single pass and exit, instead of looping") 24 listen := flag.String("listen", ":8090", "address to serve /healthz and / (status) on; empty disables it") 25 flag.Parse() 26 27 log := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 28 29 cfg, err := config.Load(*configPath) 30 if err != nil { 31 log.Error("load config", "err", err) 32 os.Exit(1) 33 } 34 35 st, err := state.Open(cfg.StateDB) 36 if err != nil { 37 log.Error("open state db", "err", err) 38 os.Exit(1) 39 } 40 defer st.Close() 41 42 stateDir := cfg.StateDB 43 if idx := lastSlash(stateDir); idx >= 0 { 44 stateDir = stateDir[:idx] 45 } 46 47 syncers := make([]*sync.RepoSyncer, 0, len(cfg.Repos)) 48 for _, pair := range cfg.Repos { 49 rs, err := sync.New(pair, st, sync.WorkDirFor(stateDir, pair.Name)) 50 if err != nil { 51 log.Error("set up repo pair", "pair", pair.Name, "err", err) 52 os.Exit(1) 53 } 54 syncers = append(syncers, rs) 55 } 56 57 tracker := status.NewTracker(st, cfg.SourceURL) 58 tracker.SetTopology(buildTopology(cfg)) 59 runAll := func() { 60 for _, rs := range syncers { 61 gitErr, issuesErr, patchErr := rs.Run(log) 62 tracker.Record(rs.Name(), rs.Series(), gitErr, issuesErr, patchErr) 63 } 64 } 65 66 if *listen != "" { 67 go func() { 68 if err := http.ListenAndServe(*listen, tracker.Handler()); err != nil { 69 log.Error("status server stopped", "err", err) 70 } 71 }() 72 } 73 74 runAll() 75 if *once { 76 return 77 } 78 79 ticker := time.NewTicker(cfg.SyncInterval) 80 defer ticker.Stop() 81 for range ticker.C { 82 runAll() 83 } 84} 85 86// buildTopology declares every mirror target configured for each series 87// (its Forgejo side and its Radicle side), so the dashboard can show a 88// side that's never produced an event yet — not just sides the activity 89// log happens to mention. 90func buildTopology(cfg *config.Config) map[string][]status.ServerRef { 91 topology := map[string][]status.ServerRef{} 92 seen := map[string]bool{} 93 add := func(series, host, repoURL string) { 94 if host == "" { 95 return 96 } 97 key := series + "|" + host 98 if seen[key] { 99 return 100 } 101 seen[key] = true 102 topology[series] = append(topology[series], status.ServerRef{Label: host, URL: repoURL}) 103 } 104 105 for _, pair := range cfg.Repos { 106 series := pair.Series 107 if series == "" { 108 series = pair.Name 109 } 110 111 fHost := "" 112 if u, err := url.Parse(pair.Forgejo.BaseURL); err == nil { 113 fHost = u.Host 114 } 115 fURL := strings.TrimRight(pair.Forgejo.BaseURL, "/") + "/" + pair.Forgejo.Owner + "/" + pair.Forgejo.Repo 116 add(series, fHost, fURL) 117 118 rHost := "" 119 if u, err := url.Parse(pair.Radicle.HTTPBaseURL); err == nil { 120 rHost = u.Host 121 } 122 add(series, rHost, sync.RadicleExplorerLink(pair.Radicle)) 123 } 124 return topology 125} 126 127func lastSlash(s string) int { 128 for i := len(s) - 1; i >= 0; i-- { 129 if s[i] == '/' { 130 return i 131 } 132 } 133 return -1 134}