package service import ( "encoding/json" "errors" "fmt" "math/rand" "net/url" "os" "strconv" "strings" "sync" "time" "git.server.ky/slackcoder/mirror/internal/github" ) type Mirror struct { Method string `json:"method"` From *url.URL `json:"-"` To *url.URL `json:"-"` Description string `json:"description,omitempty"` } func (m *Mirror) Equal(arg *Mirror) bool { return m.Method == arg.Method && m.From.String() == arg.From.String() && m.To.String() == arg.To.String() } func (m *Mirror) String() string { buf, err := json.Marshal(m) if err != nil { panic(err) } return string(buf) } type Service struct { cfg *Config mirrorsLock sync.Mutex mirrors []*Mirror schedule []time.Time stopCh chan struct{} } func NewService(cfg *Config) (*Service, error) { cfg.Apply(DefaultConfig) srv := &Service{ cfg: cfg, mirrors: nil, stopCh: make(chan struct{}), } for _, m := range cfg.Mirrors { err := srv.AddMirror(m) if err != nil { return nil, err } } return srv, nil } func (s *Service) scheduleNextRun(arg *Mirror) { // Be polite. period := s.cfg.MaxInterval.Duration - s.cfg.MinInterval.Duration at := time.Now().Add(s.cfg.MinInterval.Duration + time.Duration(rand.Intn(int(period)))) for i, m := range s.mirrors { if m.Equal(arg) { s.schedule[i] = at } } } func (s *Service) AddMirror(arg *Mirror) error { defer s.mirrorsLock.Unlock() s.mirrorsLock.Lock() for _, m := range s.mirrors { if m.Equal(arg) { return errors.New("must be unique") } } s.schedule = append(s.schedule, time.Time{}) s.mirrors = append(s.mirrors, arg) s.scheduleNextRun(arg) return nil } func (s *Service) scheduled(yield func(*Mirror) bool) { defer s.mirrorsLock.Unlock() s.mirrorsLock.Lock() now := time.Now() for i, m := range s.mirrors { if s.schedule[i].After(now) { continue } if !yield(m) { break } } } func (s *Service) Mirror(arg *Mirror) error { if arg.From.Path == "" || arg.To.Path == "" || arg.Method == "" { return fmt.Errorf("badly formatted mirror '%s'", arg.String()) } var err error switch arg.Method { case "git": err = MirrorGit(arg.To, arg.From, arg.Description) case "github-assets": client := github.NewClient() err = client.MirrorAssets(arg.To, arg.From) case "rsync": err = Rsync(arg.To, arg.From) default: err = fmt.Errorf("unknown method '%s'", arg.Method) } if err != nil { return fmt.Errorf("could not clone from '%s': %w", arg.From, err) } return nil } func (s *Service) RemoveMirror(arg *Mirror) error { defer s.mirrorsLock.Unlock() s.mirrorsLock.Lock() for i, m := range s.mirrors { if m.Equal(arg) { s.mirrors[i] = s.mirrors[len(s.mirrors)-1] s.mirrors = s.mirrors[:len(s.mirrors)-1] s.schedule[i] = s.schedule[len(s.schedule)-1] s.schedule = s.schedule[:len(s.schedule)-1] return nil } } return errors.New("not found") } func (s *Service) Log(str string) { escaped := []rune(strconv.Quote(strings.TrimSpace(str))) escaped = escaped[1 : len(escaped)-1] fmt.Fprintf(os.Stderr, "%s: %s\n", time.Now().Format(time.RFC822Z), string(escaped)) } func (s *Service) Run() error { wakeup := time.NewTicker(time.Second) mainLoop: for { select { case <-wakeup.C: case <-s.stopCh: break mainLoop } s.scheduled(func(m *Mirror) bool { err := s.Mirror(m) if err != nil { s.Log(err.Error()) } s.scheduleNextRun(m) return true }) } return nil } func (s *Service) Stop() { select { case <-s.stopCh: default: close(s.stopCh) } }