package sync import ( "cloudsave/pkg/data" "cloudsave/pkg/remote" "cloudsave/pkg/remote/client" "cloudsave/pkg/repository" "errors" "fmt" ) type ( ConflictResolution int State int decision int Syncer struct { cli *client.Client service *data.Service stateCallback func(s State, g repository.Metadata) errorCallback func(err error, g repository.Metadata) conflictCallback func(a, b repository.Metadata) ConflictResolution } ) const ( Their ConflictResolution = iota Mine None ) const ( ignore decision = iota push pull ) const ( FetchingMetdata State = iota Pushing Pulling Pushed Pulled UpToDate ) var ( ErrFetching error = errors.New("failed to fetch the metadata") ErrPushing error = errors.New("failed to push data") ErrPulling error = errors.New("failed to pull data") ErrDatastore error = errors.New("failed to get data from local datastore") ) func NewSyncer(cli *client.Client, service *data.Service) *Syncer { return &Syncer{ cli: cli, service: service, } } func (s *Syncer) SetStateCallback(fn func(s State, g repository.Metadata)) { s.stateCallback = fn } func (s *Syncer) SetErrorCallback(fn func(err error, g repository.Metadata)) { s.errorCallback = fn } func (s *Syncer) SetConflictCallback(fn func(a, b repository.Metadata) ConflictResolution) { s.conflictCallback = fn } func (s *Syncer) Sync() { games, err := s.service.AllGames() if err != nil { s.errorCallback(fmt.Errorf("failed to get all games: %w", err), repository.Metadata{}) return } for _, g := range games { r, err := remote.One(g.ID) if err != nil { s.errorCallback(fmt.Errorf("%w: %s", ErrDatastore, err), g) } if r.URL != s.cli.BaseURL() { continue } if err := s.sync(g); err != nil { s.errorCallback(err, g) } } } func (s *Syncer) sync(g repository.Metadata) error { s.stateCallback(FetchingMetdata, g) remoteMetadata, err := s.cli.Metadata(g.ID) if err != nil { if errors.Is(err, client.ErrNotFound) { s.stateCallback(Pushing, g) if err := s.push(g); err != nil { return fmt.Errorf("%w: %s", ErrPushing, err) } s.stateCallback(Pushed, g) return nil } return fmt.Errorf("%w: %s", ErrFetching, err) } if g.MD5 == remoteMetadata.MD5 { s.stateCallback(UpToDate, g) return nil } d := ignore if g.Version > remoteMetadata.Version { d = push } if g.Version < remoteMetadata.Version { d = pull } if g.Version == remoteMetadata.Version { r := s.conflictCallback(g, remoteMetadata) switch r { case Mine: { d = push } case Their: { d = pull } } return nil } switch d { case push: { s.stateCallback(Pushing, g) if err := s.push(g); err != nil { return fmt.Errorf("%w: %s", ErrPushing, err) } s.stateCallback(Pushed, g) return nil } case pull: { s.stateCallback(Pulling, g) if err := s.pull(g, remoteMetadata); err != nil { return fmt.Errorf("%w: %s", ErrPulling, err) } s.stateCallback(Pulled, g) return nil } } return nil } func (s *Syncer) push(g repository.Metadata) error { if err := s.service.PushArchive(g.ID, "", s.cli); err != nil { return err } // manage backup bs, err := s.service.AllBackups(g.ID) if err != nil { return err } for _, b := range bs { binfo, err := s.cli.ArchiveInfo(g.ID, b.UUID) if err != nil { if !errors.Is(err, client.ErrNotFound) { return fmt.Errorf("failed to get remote information about the backup file: %w", err) } } if binfo.MD5 != b.MD5 { if err := s.cli.PushBackup(b, g); err != nil { return fmt.Errorf("failed to push backup: %w", err) } } } return nil } func (s *Syncer) pull(g, r repository.Metadata) error { g.Version = r.Version g.Date = r.Date if err := s.service.UpdateMetadata(g.ID, g); err != nil { return err } if err := s.service.PullArchive(g.ID, "", s.cli); err != nil { return err } if err := s.service.ApplyCurrent(g.ID); err != nil { return err } // manage backup bs, err := s.cli.ListArchives(g.ID) if err != nil { return err } for _, uuid := range bs { rinfo, err := s.cli.ArchiveInfo(g.ID, uuid) if err != nil { return err } linfo, err := s.service.Backup(g.ID, uuid) if err != nil { return err } if linfo.MD5 != rinfo.MD5 { if err := s.service.PullBackup(g.ID, uuid, s.cli); err != nil { return err } } } return nil }