diff options
Diffstat (limited to 'internal/service/service.go')
-rw-r--r-- | internal/service/service.go | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..c34bcd3 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,195 @@ +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) + } +} |