all repos

mugit @ 9c82bda

🐮 git server that your cow will love

mugit/internal/mirror/mirror.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
mirror: increase amount of workers; small refactoring, 4 months ago
1
package mirror
2
3
import (
4
	"context"
5
	"errors"
6
	"fmt"
7
	"log/slog"
8
	"os"
9
	"path/filepath"
10
	"strings"
11
	"sync"
12
	"time"
13
14
	"golang.org/x/sync/semaphore"
15
	"olexsmir.xyz/mugit/internal/config"
16
	"olexsmir.xyz/mugit/internal/git"
17
)
18
19
type Worker struct {
20
	c *config.Config
21
}
22
23
func NewWorker(cfg *config.Config) *Worker {
24
	return &Worker{
25
		c: cfg,
26
	}
27
}
28
29
func (w *Worker) Start(ctx context.Context) error {
30
	interval, err := time.ParseDuration(w.c.Mirror.Interval)
31
	if err != nil {
32
		slog.Error("couldn't parse interval time", "err", err)
33
		return err
34
	}
35
36
	ticker := time.NewTicker(interval)
37
	defer ticker.Stop()
38
39
	for {
40
		select {
41
		case <-ctx.Done():
42
			return nil
43
		default:
44
			if err := w.mirror(ctx); err != nil {
45
				slog.Error("mirror sync failed", "err", err)
46
			}
47
48
			<-ticker.C
49
		}
50
	}
51
}
52
53
func (w *Worker) mirror(ctx context.Context) error {
54
	repos, err := w.findMirrorRepos()
55
	if err != nil {
56
		return err
57
	}
58
59
	var wg sync.WaitGroup
60
	sem := semaphore.NewWeighted(10)
61
	errCh := make(chan error, len(repos))
62
63
	for _, repo := range repos {
64
		wg.Go(func() {
65
			if err := sem.Acquire(ctx, 1); err != nil {
66
				errCh <- err
67
				return
68
			}
69
			defer sem.Release(1)
70
71
			if err := w.syncRepo(ctx, repo); err != nil {
72
				errCh <- err
73
			}
74
		})
75
	}
76
77
	wg.Wait()
78
	close(errCh)
79
80
	var errs []error
81
	for err := range errCh {
82
		errs = append(errs, err)
83
	}
84
	return errors.Join(errs...)
85
}
86
87
func (w *Worker) syncRepo(_ context.Context, repo *git.Repo) error {
88
	name := repo.Name()
89
	slog.Info("mirror: sync started", "repo", name)
90
91
	remoteURL, err := repo.RemoteURL()
92
	if err != nil {
93
		slog.Error("mirror: failed to get remote url", "repo", name, "err", err)
94
		return err
95
	}
96
97
	if err := w.isSupportedRemote(remoteURL); err != nil {
98
		slog.Error("mirror: remote is not valid", "repo", name, "err", err)
99
		return err
100
	}
101
102
	if w.isRemoteGithub(remoteURL) && w.c.Mirror.GithubToken != "" {
103
		if err := repo.FetchFromGithubWithToken(w.c.Mirror.GithubToken); err != nil {
104
			slog.Error("mirror: fetch failed (github)", "repo", name, "err", err)
105
			return err
106
		}
107
	} else {
108
		if err := repo.Fetch(); err != nil {
109
			slog.Error("mirror: fetch failed", "repo", name, "err", err)
110
			return err
111
		}
112
	}
113
114
	if err := repo.SetLastSync(time.Now()); err != nil {
115
		slog.Error("mirror: failed to set last sync time", "repo", name, "err", err)
116
	}
117
118
	slog.Info("mirror: sync completed", "repo", repo.Name())
119
	return nil
120
}
121
122
func (w *Worker) findMirrorRepos() ([]*git.Repo, error) {
123
	dirs, err := os.ReadDir(w.c.Repo.Dir)
124
	if err != nil {
125
		return nil, err
126
	}
127
128
	var repos []*git.Repo
129
	for _, dir := range dirs {
130
		if !dir.IsDir() {
131
			continue
132
		}
133
134
		name := dir.Name()
135
		path := filepath.Join(w.c.Repo.Dir, filepath.Clean(name))
136
		repo, err := git.Open(path, "")
137
		if err != nil {
138
			slog.Debug("skipping non-git directory", "name", name, "err", err)
139
			continue
140
		}
141
142
		isMirror, err := repo.IsMirror()
143
		if err != nil {
144
			slog.Debug("skipping non-mirror repo", "name", name, "err", err)
145
			continue
146
		}
147
148
		if isMirror {
149
			repos = append(repos, repo)
150
		}
151
	}
152
153
	return repos, nil
154
}
155
156
func (w *Worker) isSupportedRemote(remote string) error {
157
	if !strings.HasPrefix(remote, "http") {
158
		return fmt.Errorf("only http and https remotes are supported")
159
	}
160
	return nil
161
}
162
163
func (w *Worker) isRemoteGithub(remoteURL string) bool {
164
	return strings.Contains(remoteURL, "github.com")
165
}