package main import ( "flag" "github.com/cheggaaa/pb/v3" "github.com/udhos/equalfile" "log" "os" "path" ) type TopLevel struct { LeftInput string RightInput string LeftOutput string CombinedOutput string RightOutput string DryRun bool Bar *pb.ProgressBar Comparer *equalfile.Cmp } type Step struct { *TopLevel Subpath string } type Direction byte const ( LEFT Direction = iota RIGHT COMBINED ) type FileState byte const ( MISSING FileState = iota FILE DIRECTORY UNKNOWN ) func (s *Step) CheckState(filePath string) (out FileState, err error) { info, err := os.Stat(filePath) if err != nil { if os.IsNotExist(err) { return MISSING, nil } else { return UNKNOWN, err } } if info.IsDir() { return DIRECTORY, nil } else if info.Mode().IsRegular() { return FILE, nil } else { return UNKNOWN, nil } } func (s *Step) InputPath(child string, side Direction) string { var basePath string switch side { case LEFT: basePath = s.LeftInput case RIGHT: basePath = s.RightInput default: panic("Unexpected side for input path") } return path.Join(basePath, s.Subpath, child) } func (s *Step) OutputPath(child string, side Direction) string { var basePath string switch side { case LEFT: basePath = s.LeftOutput case RIGHT: basePath = s.RightOutput case COMBINED: basePath = s.CombinedOutput default: panic("Unexpected side for output path") } return path.Join(basePath, s.Subpath, child) } func (s *Step) Separate(child string) { s.SeparateLeft(child) s.SeparateRight(child) } func (s *Step) SeparateLeft(child string) { leftInPath := s.InputPath(child, LEFT) leftOutPath := s.OutputPath(child, LEFT) if s.DryRun { return } leftBase := path.Dir(leftOutPath) err := os.MkdirAll(leftBase, 0755) if err != nil { log.Printf("Failed creating %s: %s\n", leftBase, err) } err = os.Rename(leftInPath, leftOutPath) if err != nil && !os.IsNotExist(err) { log.Printf("Failed moving %s to %s: %s\n", leftInPath, leftOutPath, err) } } func (s *Step) SeparateRight(child string) { rightInPath := s.InputPath(child, RIGHT) rightOutPath := s.OutputPath(child, RIGHT) if s.DryRun { return } rightBase := path.Dir(rightOutPath) err := os.MkdirAll(rightBase, 0755) if err != nil { log.Printf("Failed creating %s: %s\n", rightBase, err) } err = os.Rename(rightInPath, rightOutPath) if err != nil && !os.IsNotExist(err) { log.Printf("Failed moving %s to %s: %s\n", rightInPath, rightOutPath, err) } } func (s *Step) Combine(child string) { leftInPath := s.InputPath(child, LEFT) rightInPath := s.InputPath(child, RIGHT) combinedOutPath := s.OutputPath(child, COMBINED) if s.DryRun { return } combinedBase := path.Dir(combinedOutPath) err := os.MkdirAll(combinedBase, 0755) if err != nil { log.Printf("Failed creating %s: %s\n", combinedBase, err) } err = os.Rename(leftInPath, combinedOutPath) if err != nil && !os.IsNotExist(err) { log.Printf("Failed moving %s to %s: %s\n", leftInPath, combinedOutPath, err) } err = os.Remove(rightInPath) if err != nil && !os.IsNotExist(err) { log.Printf("Failed removing %s: %s\n", rightInPath, err) } } func (s *Step) MakeCombinedDir(child string) { combinedOutPath := s.OutputPath(child, COMBINED) if s.DryRun { return } err := os.MkdirAll(combinedOutPath, 0755) if err != nil { log.Printf("Failed creating %s: %s\n", combinedOutPath, err) } } func (s *Step) RemoveInputDirs(child string) { leftInPath := s.InputPath(child, LEFT) rightInPath := s.InputPath(child, RIGHT) if s.DryRun { return } err := os.Remove(leftInPath) if err != nil && !os.IsNotExist(err) { log.Printf("Failed removing %s: %s\n", leftInPath, err) } err = os.Remove(rightInPath) if err != nil && !os.IsNotExist(err) { log.Printf("Failed removing %s: %s\n", rightInPath, err) } } func (s *Step) ListChildren() []string { leftInPath := s.InputPath("", LEFT) rightInPath := s.InputPath("", RIGHT) results := make(map[string]bool) leftFiles, err := os.ReadDir(leftInPath) if err != nil { log.Printf("Failed listing %s: %s\n", leftInPath, err) } rightFiles, err := os.ReadDir(rightInPath) if err != nil { log.Printf("Failed listing %s: %s\n", rightInPath, err) } for _, file := range leftFiles { results[file.Name()] = true } for _, file := range rightFiles { results[file.Name()] = true } out := make([]string, 0, len(results)) for file := range results { out = append(out, file) } return out } func (s *Step) AreFilesIdentical(child string) bool { leftInPath := s.InputPath(child, LEFT) rightInPath := s.InputPath(child, RIGHT) equal, err := s.Comparer.CompareFile(leftInPath, rightInPath) if err != nil { log.Printf("Error comparing path %s and %s: %s\n", leftInPath, rightInPath, err) return false } return equal } func (s *Step) Walk() { children := s.ListChildren() s.Bar.AddTotal(int64(len(children))) for _, child := range children { rightPath := s.InputPath(child, RIGHT) rightState, err := s.CheckState(rightPath) if err != nil { log.Printf("Error statting path %s: %s\n", rightPath, err) s.Bar.Increment() continue } else if rightState == UNKNOWN { log.Printf("Unknown stat value for path %s\n", rightPath) s.Bar.Increment() continue } else if rightState == MISSING { s.SeparateLeft(child) s.Bar.Increment() continue } leftPath := s.InputPath(child, LEFT) leftState, err := s.CheckState(leftPath) if err != nil { log.Printf("Error statting path %s: %s\n", leftPath, err) s.Bar.Increment() continue } else if leftState == UNKNOWN { log.Printf("Unknown stat value for path %s\n", leftPath) s.Bar.Increment() continue } else if leftState == MISSING { s.SeparateRight(child) s.Bar.Increment() continue } switch rightState { case FILE: if leftState == FILE { if s.AreFilesIdentical(child) { s.Combine(child) s.Bar.Increment() } else { s.Separate(child) s.Bar.Increment() } } else { s.Separate(child) s.Bar.Increment() } case DIRECTORY: if leftState == DIRECTORY { substep := Step{ TopLevel: s.TopLevel, Subpath: path.Join(s.Subpath, child), } s.MakeCombinedDir(child) substep.Walk() s.RemoveInputDirs(child) s.Bar.Increment() } else { s.Separate(child) s.Bar.Increment() } default: panic("Unexpected state") } } } func main() { settings := TopLevel{ Bar: pb.StartNew(1), Comparer: equalfile.New(nil, equalfile.Options{}), } flag.StringVar(&settings.LeftInput, "left-input", "./input/left", "The name of the left side of the input.") flag.StringVar(&settings.RightInput, "right-input", "./input/right", "The name of the right side of the input.") flag.StringVar(&settings.LeftOutput, "left-output", "./output/left", "The name of the left side of the output.") flag.StringVar(&settings.CombinedOutput, "combined-output", "./output/combined", "The name of the combined side of the output.") flag.StringVar(&settings.RightOutput, "right-output", "./output/right", "The name of the right side of the output.") flag.BoolVar(&settings.DryRun, "dry-run", true, "True if no actual operation should be performed.") flag.Parse() (&Step{ TopLevel: &settings, Subpath: "", }).Walk() settings.Bar.Increment() }