package recovery import ( "errors" "fmt" "os" "path/filepath" "regexp" "sort" "strconv" ) var ( reChanges = regexp.MustCompile(`(\d+)\.changes`) reSnapshot = regexp.MustCompile(`(\d+)\.snapshot`) ) func joinChangesFileName(dir string, logNumber int) string { return filepath.Join(dir, fmt.Sprintf("%d.changes", logNumber)) } type RecoveryRecipe struct { Snapshot string Changes []string LogNumber int ToDelete []string CompleteSnapshot bool // флаг - что нужно завершить создание снапшота } type RecoveryAdvisor struct { dir string verifySnapshot func(string) (bool, error) // (fileName) isVerified, error } type RecoveryAdvisorOptions struct { Dir string VerifySnapshot func(string) (bool, error) } func NewRecoveryAdvisor(opt RecoveryAdvisorOptions) (*RecoveryAdvisor, error) { if opt.Dir == "" { return nil, errors.New("Dir option is required") } if opt.VerifySnapshot == nil { return nil, errors.New("VerifySnapshot option is required") } return &RecoveryAdvisor{ dir: opt.Dir, verifySnapshot: opt.VerifySnapshot, }, nil } type SnapshotChangesPair struct { SnapshotFileName string ChangesFileName string LogNumber int } func (s *RecoveryAdvisor) getSnapshotChangesPairs() (*SnapshotChangesPair, *SnapshotChangesPair, error) { var ( numSet = make(map[int]bool) changesSet = make(map[int]bool) snapshotsSet = make(map[int]bool) pairs []SnapshotChangesPair ) entries, err := os.ReadDir(s.dir) if err != nil { return nil, nil, err } for _, entry := range entries { if entry.Type().IsRegular() { baseName := entry.Name() groups := reChanges.FindStringSubmatch(baseName) if len(groups) == 2 { num, _ := strconv.Atoi(groups[1]) numSet[num] = true changesSet[num] = true } groups = reSnapshot.FindStringSubmatch(baseName) if len(groups) == 2 { num, _ := strconv.Atoi(groups[1]) numSet[num] = true snapshotsSet[num] = true } } } for logNumber := range numSet { var ( snapshotFileName string changesFileName string ) if changesSet[logNumber] { changesFileName = joinChangesFileName(s.dir, logNumber) } if snapshotsSet[logNumber] { snapshotFileName = filepath.Join(s.dir, fmt.Sprintf("%d.snapshot", logNumber)) } pairs = append(pairs, SnapshotChangesPair{ ChangesFileName: changesFileName, SnapshotFileName: snapshotFileName, LogNumber: logNumber, }) } if len(pairs) == 0 { return nil, nil, nil } sort.Slice(pairs, func(i, j int) bool { return pairs[i].LogNumber > pairs[j].LogNumber }) pair := pairs[0] if pair.ChangesFileName == "" { return nil, nil, fmt.Errorf("has %d.shapshot file, but %d.changes file not found", pair.LogNumber, pair.LogNumber) } if len(pairs) > 1 { prevPair := pairs[1] if prevPair.SnapshotFileName == "" && prevPair.LogNumber != 1 { return &pair, nil, nil } if prevPair.ChangesFileName == "" && pair.SnapshotFileName == "" { return &pair, nil, nil } return &pair, &prevPair, nil } else { return &pair, nil, nil } } func (s *RecoveryAdvisor) GetRecipe() (*RecoveryRecipe, error) { pair, prevPair, err := s.getSnapshotChangesPairs() if err != nil { return nil, err } if pair == nil { return nil, nil } if pair.SnapshotFileName != "" { isVerified, err := s.verifySnapshot(pair.SnapshotFileName) if err != nil { return nil, fmt.Errorf("verifySnapshot %s: %s", pair.SnapshotFileName, err) } if isVerified { recipe := &RecoveryRecipe{ Snapshot: pair.SnapshotFileName, Changes: []string{ pair.ChangesFileName, }, LogNumber: pair.LogNumber, } if prevPair != nil { if prevPair.ChangesFileName != "" { recipe.ToDelete = append(recipe.ToDelete, prevPair.ChangesFileName) } if prevPair.SnapshotFileName != "" { recipe.ToDelete = append(recipe.ToDelete, prevPair.SnapshotFileName) } } return recipe, nil } if prevPair != nil { return s.tryPrevPair(pair, prevPair) } return nil, fmt.Errorf("%d.shapshot is corrupted", pair.LogNumber) } else { if prevPair != nil { return s.tryPrevPair(pair, prevPair) } else { if pair.LogNumber == 1 { return &RecoveryRecipe{ Changes: []string{ pair.ChangesFileName, }, LogNumber: pair.LogNumber, }, nil } else { return nil, fmt.Errorf("%d.snapshot not found", pair.LogNumber) } } } } func (s *RecoveryAdvisor) tryPrevPair(pair, prevPair *SnapshotChangesPair) (*RecoveryRecipe, error) { if prevPair.ChangesFileName == "" { if pair.SnapshotFileName != "" { return nil, fmt.Errorf("%d.shapshot is corrupted and %d.changes not found", pair.LogNumber, prevPair.LogNumber) } else { return nil, fmt.Errorf("%d.changes not found", prevPair.LogNumber) } } if prevPair.SnapshotFileName == "" { if prevPair.LogNumber == 1 { recipe := &RecoveryRecipe{ Changes: []string{ prevPair.ChangesFileName, pair.ChangesFileName, }, LogNumber: pair.LogNumber, CompleteSnapshot: true, ToDelete: []string{ prevPair.ChangesFileName, }, } return recipe, nil } else { if pair.SnapshotFileName != "" { return nil, fmt.Errorf("%d.shapshot is corrupted and %d.snapshot not found", pair.LogNumber, prevPair.LogNumber) } else { return nil, fmt.Errorf("%d.snapshot not found", pair.LogNumber) } } } isVerified, err := s.verifySnapshot(prevPair.SnapshotFileName) if err != nil { return nil, fmt.Errorf("verifySnapshot %s: %s", prevPair.SnapshotFileName, err) } if !isVerified { return nil, fmt.Errorf("%d.shapshot is corrupted", prevPair.LogNumber) } recipe := &RecoveryRecipe{ Snapshot: prevPair.SnapshotFileName, Changes: []string{ prevPair.ChangesFileName, pair.ChangesFileName, }, LogNumber: pair.LogNumber, CompleteSnapshot: true, ToDelete: []string{ prevPair.ChangesFileName, prevPair.SnapshotFileName, }, } return recipe, nil }