// Package mirror implements pull/push mirroring: periodically running // `git fetch --prune --tags ` (pull) or `git push --mirror -f // ` (push) against a remote configured via `git remote add // --mirror=fetch|push`, mirroring services/mirror/mirror_{pull,push}.go. package mirror import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // Direction is which way a mirror syncs. type Direction string const ( Pull Direction = "pull" Push Direction = "push" ) // remoteName is the git remote name nugitea uses for every configured // mirror; each repo has at most one mirror per direction. const remoteName = "mirror" // Entry is one configured mirror. type Entry struct { Repo string `json:"repo"` RemoteURL string `json:"remote_url"` Direction Direction `json:"direction"` IntervalSeconds int64 `json:"interval_seconds"` LastSync time.Time `json:"last_sync"` } func (e *Entry) key() string { return string(e.Direction) + ":" + e.Repo } // Store persists mirror configuration to a JSON file under the repo root. type Store struct { path string mu sync.Mutex } // NewStore returns a Store backed by mirrors.json under root. func NewStore(root string) *Store { return &Store{path: filepath.Join(root, "mirrors.json")} } func (s *Store) load() ([]Entry, error) { data, err := os.ReadFile(s.path) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } var entries []Entry if err := json.Unmarshal(data, &entries); err != nil { return nil, err } return entries, nil } func (s *Store) save(entries []Entry) error { data, err := json.MarshalIndent(entries, "", " ") if err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } return os.Rename(tmp, s.path) } // Add registers a new mirror, replacing any existing one with the same // repo+direction. func (s *Store) Add(e Entry) error { s.mu.Lock() defer s.mu.Unlock() entries, err := s.load() if err != nil { return err } out := entries[:0] for _, existing := range entries { if existing.key() != e.key() { out = append(out, existing) } } out = append(out, e) return s.save(out) } // All returns every configured mirror. func (s *Store) All() ([]Entry, error) { s.mu.Lock() defer s.mu.Unlock() return s.load() } // touch updates LastSync for the mirror matching key to now. func (s *Store) touch(e Entry, when time.Time) error { s.mu.Lock() defer s.mu.Unlock() entries, err := s.load() if err != nil { return err } found := false for i := range entries { if entries[i].key() == e.key() { entries[i].LastSync = when found = true } } if !found { return fmt.Errorf("mirror %s not found", e.key()) } return s.save(entries) }