diff --git a/instruction.go b/instruction.go index 281b730..5da64ea 100644 --- a/instruction.go +++ b/instruction.go @@ -1,132 +1,132 @@ -package main - -import ( - "fmt" - "log" - "os" - "regexp" - "strconv" - "strings" -) - -type LinkInstruction struct { - Source string - Target string - Force bool -} - -func (instruction *LinkInstruction) String() string { - return fmt.Sprintf("%s%s%s%s%s%s%s%s%s", SourceColor, instruction.Source, DefaultColor, deliminer, TargetColor, instruction.Target, DefaultColor, deliminer, strconv.FormatBool(instruction.Force)) -} - -func ParseInstruction(line string) (LinkInstruction, error) { - parts := strings.Split(line, deliminer) - instruction := LinkInstruction{} - - if len(parts) < 2 { - return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)") - } - - instruction.Source = parts[0] - instruction.Target = parts[1] - instruction.Force = false - if len(parts) > 2 { - res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2]) - instruction.Force = res - } - - instruction.Source, _ = ConvertHome(instruction.Source) - instruction.Target, _ = ConvertHome(instruction.Target) - - instruction.Source = NormalizePath(instruction.Source) - instruction.Target = NormalizePath(instruction.Target) - - return instruction, nil -} - -func (instruction *LinkInstruction) RunSync() error { - if !FileExists(instruction.Source) { - return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor) - } - - if AreSame(instruction.Source, instruction.Target) { - log.Printf("Source %s%s%s and target %s%s%s are the same, %snothing to do...%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, PathColor, DefaultColor) - return nil - } - - if FileExists(instruction.Target) { - if instruction.Force { - isSymlink, err := IsSymlink(instruction.Target) - if err != nil { - return fmt.Errorf("could not determine whether %s%s%s is a sym link or not, stopping; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) - } - - if isSymlink { - log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor) - err = os.Remove(instruction.Target) - if err != nil { - return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) - } - } else { - return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor) - } - } else { - return fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor) - } - } - - err := os.Symlink(instruction.Source, instruction.Target) - if err != nil { - return fmt.Errorf("failed creating symlink between %s%s%s and %s%s%s with error %s%+v%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) - } - log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor) - - return nil -} - -func (instruction *LinkInstruction) RunAsync(status chan (error)) { - defer close(status) - if !FileExists(instruction.Source) { - status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor) - return - } - - if AreSame(instruction.Source, instruction.Target) { - status <- fmt.Errorf("source %s%s%s and target %s%s%s are the same, %snothing to do...%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, PathColor, DefaultColor) - return - } - - if FileExists(instruction.Target) { - if instruction.Force { - isSymlink, err := IsSymlink(instruction.Target) - if err != nil { - status <- fmt.Errorf("could not determine whether %s%s%s is a sym link or not, stopping; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) - return - } - - if isSymlink { - log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor) - err = os.Remove(instruction.Target) - if err != nil { - status <- fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) - return - } - } else { - status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor) - return - } - } else { - status <- fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor) - return - } - } - - err := os.Symlink(instruction.Source, instruction.Target) - if err != nil { - status <- fmt.Errorf("failed creating symlink between %s%s%s and %s%s%s with error %s%+v%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) - return - } - log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor) - - status <- nil -} \ No newline at end of file +package main + +import ( + "fmt" + "log" + "os" + "regexp" + "strconv" + "strings" +) + +type LinkInstruction struct { + Source string + Target string + Force bool +} + +func (instruction *LinkInstruction) String() string { + return fmt.Sprintf("%s%s%s%s%s%s%s%s%s", SourceColor, instruction.Source, DefaultColor, deliminer, TargetColor, instruction.Target, DefaultColor, deliminer, strconv.FormatBool(instruction.Force)) +} + +func ParseInstruction(line string) (LinkInstruction, error) { + parts := strings.Split(line, deliminer) + instruction := LinkInstruction{} + + if len(parts) < 2 { + return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)") + } + + instruction.Source = parts[0] + instruction.Target = parts[1] + instruction.Force = false + if len(parts) > 2 { + res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2]) + instruction.Force = res + } + + instruction.Source, _ = ConvertHome(instruction.Source) + instruction.Target, _ = ConvertHome(instruction.Target) + + instruction.Source = NormalizePath(instruction.Source) + instruction.Target = NormalizePath(instruction.Target) + + return instruction, nil +} + +func (instruction *LinkInstruction) RunSync() error { + if !FileExists(instruction.Source) { + return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor) + } + + if AreSame(instruction.Source, instruction.Target) { + log.Printf("Source %s%s%s and target %s%s%s are the same, %snothing to do...%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, PathColor, DefaultColor) + return nil + } + + if FileExists(instruction.Target) { + if instruction.Force { + isSymlink, err := IsSymlink(instruction.Target) + if err != nil { + return fmt.Errorf("could not determine whether %s%s%s is a sym link or not, stopping; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) + } + + if isSymlink { + log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor) + err = os.Remove(instruction.Target) + if err != nil { + return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) + } + } else { + return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor) + } + } else { + return fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor) + } + } + + err := os.Symlink(instruction.Source, instruction.Target) + if err != nil { + return fmt.Errorf("failed creating symlink between %s%s%s and %s%s%s with error %s%+v%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) + } + log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor) + + return nil +} + +func (instruction *LinkInstruction) RunAsync(status chan (error)) { + defer close(status) + if !FileExists(instruction.Source) { + status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor) + return + } + + if AreSame(instruction.Source, instruction.Target) { + status <- fmt.Errorf("source %s%s%s and target %s%s%s are the same, %snothing to do...%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, PathColor, DefaultColor) + return + } + + if FileExists(instruction.Target) { + if instruction.Force { + isSymlink, err := IsSymlink(instruction.Target) + if err != nil { + status <- fmt.Errorf("could not determine whether %s%s%s is a sym link or not, stopping; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) + return + } + + if isSymlink { + log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor) + err = os.Remove(instruction.Target) + if err != nil { + status <- fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) + return + } + } else { + status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor) + return + } + } else { + status <- fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor) + return + } + } + + err := os.Symlink(instruction.Source, instruction.Target) + if err != nil { + status <- fmt.Errorf("failed creating symlink between %s%s%s and %s%s%s with error %s%+v%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) + return + } + log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor) + + status <- nil +} diff --git a/main.go b/main.go index bb9c712..fcd4258 100644 --- a/main.go +++ b/main.go @@ -1,238 +1,238 @@ -package main - -import ( - "bufio" - "flag" - "io" - "log" - "os" - "regexp" - "sync" - "sync/atomic" -) - -const deliminer = "," -const ( - Black = "\033[30m" - Red = "\033[31m" - Green = "\033[32m" - Yellow = "\033[33m" - Blue = "\033[34m" - Magenta = "\033[35m" - Cyan = "\033[36m" - White = "\033[37m" -) -const SourceColor = Magenta -const TargetColor = Yellow -const ErrorColor = Red -const DefaultColor = White -const PathColor = Green - -var DirRegex, _ = regexp.Compile(`^(.+?)[/\\]sync$`) -var FileRegex, _ = regexp.Compile(`^sync$`) -var programName = os.Args[0] - -func main() { - recurse := flag.String("r", "", "recurse into directories") - file := flag.String("f", "", "file to read instructions from") - debug := flag.Bool("d", false, "debug") - flag.Parse() - - if *debug { - log.SetFlags(log.Lmicroseconds | log.Lshortfile) - logFile, err := os.Create("main.log") - if err != nil { - log.Printf("Error creating log file: %v", err) - os.Exit(1) - } - logger := io.MultiWriter(os.Stdout, logFile) - log.SetOutput(logger) - } else { - log.SetFlags(log.Lmicroseconds) - } - - log.Printf("Recurse: %s", *recurse) - log.Printf("File: %s", *file) - - instructions := make(chan LinkInstruction, 1000) - status := make(chan error) - if *recurse != "" { - go ReadFromFilesRecursively(*recurse, instructions, status) - } else if *file != "" { - go ReadFromFile(*file, instructions, status, true) - } else if len(os.Args) > 1 { - go ReadFromArgs(instructions, status) - } else { - go ReadFromStdin(instructions, status) - } - - go func() { - for { - err, ok := <-status - if !ok { - break - } - if err != nil { - log.Println(err) - } - } - }() - - var instructionsDone int32 - var wg sync.WaitGroup - for { - instruction, ok := <-instructions - if !ok { - log.Printf("No more instructions to process") - break - } - log.Printf("Processing: %s", instruction.String()) - status := make(chan error) - go instruction.RunAsync(status) - wg.Add(1) - err := <-status - if err != nil { - log.Printf("Failed parsing instruction %s%s%s due to %s%+v%s", SourceColor, instruction.String(), DefaultColor, ErrorColor, err, DefaultColor) - } - atomic.AddInt32(&instructionsDone, 1) - wg.Done() - } - wg.Wait() - log.Println("All done") - if instructionsDone == 0 { - log.Printf("No input provided") - log.Printf("Provide input as: ") - log.Printf("Arguments - %s ,,", programName) - log.Printf("File - %s -f ", programName) - log.Printf("Folder (finding sync files in folder recursively) - %s -r ", programName) - log.Printf("stdin - (cat | %s)", programName) - os.Exit(1) - } -} - -func ReadFromFilesRecursively(input string, output chan LinkInstruction, status chan error) { - defer close(output) - defer close(status) - - input = NormalizePath(input) - log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor) - - files := make(chan string, 128) - recurseStatus := make(chan error) - go GetSyncFilesRecursively(input, files, recurseStatus) - go func() { - for { - err, ok := <-recurseStatus - if !ok { - break - } - if err != nil { - log.Printf("Failed to get sync files recursively: %s%+v%s", ErrorColor, err, DefaultColor) - status <- err - } - } - }() - - var wg sync.WaitGroup - for { - file, ok := <-files - if !ok { - log.Printf("No more files to process") - break - } - wg.Add(1) - go func() { - defer wg.Done() - log.Println(file) - file = NormalizePath(file) - log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor) - - // This "has" to be done because instructions are resolved in relation to cwd - fileDir := DirRegex.FindStringSubmatch(file) - if fileDir == nil { - log.Printf("Failed to extract directory from %s%s%s", SourceColor, file, DefaultColor) - return - } - log.Printf("Changing directory to %s%s%s (for %s%s%s)", PathColor, fileDir[1], DefaultColor, PathColor, file, DefaultColor) - err := os.Chdir(fileDir[1]) - if err != nil { - log.Printf("Failed to change directory to %s%s%s: %s%+v%s", SourceColor, fileDir[1], DefaultColor, ErrorColor, err, DefaultColor) - return - } - - ReadFromFile(file, output, status, false) - }() - } - wg.Wait() -} -func ReadFromFile(input string, output chan LinkInstruction, status chan error, doclose bool) { - if doclose { - defer close(output) - defer close(status) - } - - input = NormalizePath(input) - log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor) - file, err := os.Open(input) - if err != nil { - log.Fatalf("Failed to open file %s%s%s: %s%+v%s", SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor) - return - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - instruction, err := ParseInstruction(line) - if err != nil { - log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor) - continue - } - log.Printf("Read instruction: %s", instruction.String()) - output <- instruction - } -} -func ReadFromArgs(output chan LinkInstruction, status chan error) { - defer close(output) - defer close(status) - - log.Printf("Reading input from args") - for _, arg := range os.Args[1:] { - instruction, err := ParseInstruction(arg) - if err != nil { - log.Printf("Error parsing arg: %s'%s'%s, error: %s%+v%s", SourceColor, arg, DefaultColor, ErrorColor, err, DefaultColor) - continue - } - output <- instruction - } -} -func ReadFromStdin(output chan LinkInstruction, status chan error) { - defer close(output) - defer close(status) - - log.Printf("Reading input from stdin") - - info, err := os.Stdin.Stat() - if err != nil { - log.Fatalf("Failed to stat stdin: %s%+v%s", ErrorColor, err, DefaultColor) - status <- err - return - } - if info.Mode()&os.ModeNamedPipe != 0 { - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - line := scanner.Text() - instruction, err := ParseInstruction(line) - if err != nil { - log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor) - continue - } - output <- instruction - } - if err := scanner.Err(); err != nil { - log.Fatalf("Error reading from stdin: %s%+v%s", ErrorColor, err, DefaultColor) - status <- err - return - } - } -} +package main + +import ( + "bufio" + "flag" + "io" + "log" + "os" + "regexp" + "sync" + "sync/atomic" +) + +const deliminer = "," +const ( + Black = "\033[30m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Magenta = "\033[35m" + Cyan = "\033[36m" + White = "\033[37m" +) +const SourceColor = Magenta +const TargetColor = Yellow +const ErrorColor = Red +const DefaultColor = White +const PathColor = Green + +var DirRegex, _ = regexp.Compile(`^(.+?)[/\\]sync$`) +var FileRegex, _ = regexp.Compile(`^sync$`) +var programName = os.Args[0] + +func main() { + recurse := flag.String("r", "", "recurse into directories") + file := flag.String("f", "", "file to read instructions from") + debug := flag.Bool("d", false, "debug") + flag.Parse() + + if *debug { + log.SetFlags(log.Lmicroseconds | log.Lshortfile) + logFile, err := os.Create("main.log") + if err != nil { + log.Printf("Error creating log file: %v", err) + os.Exit(1) + } + logger := io.MultiWriter(os.Stdout, logFile) + log.SetOutput(logger) + } else { + log.SetFlags(log.Lmicroseconds) + } + + log.Printf("Recurse: %s", *recurse) + log.Printf("File: %s", *file) + + instructions := make(chan LinkInstruction, 1000) + status := make(chan error) + if *recurse != "" { + go ReadFromFilesRecursively(*recurse, instructions, status) + } else if *file != "" { + go ReadFromFile(*file, instructions, status, true) + } else if len(os.Args) > 1 { + go ReadFromArgs(instructions, status) + } else { + go ReadFromStdin(instructions, status) + } + + go func() { + for { + err, ok := <-status + if !ok { + break + } + if err != nil { + log.Println(err) + } + } + }() + + var instructionsDone int32 + var wg sync.WaitGroup + for { + instruction, ok := <-instructions + if !ok { + log.Printf("No more instructions to process") + break + } + log.Printf("Processing: %s", instruction.String()) + status := make(chan error) + go instruction.RunAsync(status) + wg.Add(1) + err := <-status + if err != nil { + log.Printf("Failed parsing instruction %s%s%s due to %s%+v%s", SourceColor, instruction.String(), DefaultColor, ErrorColor, err, DefaultColor) + } + atomic.AddInt32(&instructionsDone, 1) + wg.Done() + } + wg.Wait() + log.Println("All done") + if instructionsDone == 0 { + log.Printf("No input provided") + log.Printf("Provide input as: ") + log.Printf("Arguments - %s ,,", programName) + log.Printf("File - %s -f ", programName) + log.Printf("Folder (finding sync files in folder recursively) - %s -r ", programName) + log.Printf("stdin - (cat | %s)", programName) + os.Exit(1) + } +} + +func ReadFromFilesRecursively(input string, output chan LinkInstruction, status chan error) { + defer close(output) + defer close(status) + + input = NormalizePath(input) + log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor) + + files := make(chan string, 128) + recurseStatus := make(chan error) + go GetSyncFilesRecursively(input, files, recurseStatus) + go func() { + for { + err, ok := <-recurseStatus + if !ok { + break + } + if err != nil { + log.Printf("Failed to get sync files recursively: %s%+v%s", ErrorColor, err, DefaultColor) + status <- err + } + } + }() + + var wg sync.WaitGroup + for { + file, ok := <-files + if !ok { + log.Printf("No more files to process") + break + } + wg.Add(1) + go func() { + defer wg.Done() + log.Println(file) + file = NormalizePath(file) + log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor) + + // This "has" to be done because instructions are resolved in relation to cwd + fileDir := DirRegex.FindStringSubmatch(file) + if fileDir == nil { + log.Printf("Failed to extract directory from %s%s%s", SourceColor, file, DefaultColor) + return + } + log.Printf("Changing directory to %s%s%s (for %s%s%s)", PathColor, fileDir[1], DefaultColor, PathColor, file, DefaultColor) + err := os.Chdir(fileDir[1]) + if err != nil { + log.Printf("Failed to change directory to %s%s%s: %s%+v%s", SourceColor, fileDir[1], DefaultColor, ErrorColor, err, DefaultColor) + return + } + + ReadFromFile(file, output, status, false) + }() + } + wg.Wait() +} +func ReadFromFile(input string, output chan LinkInstruction, status chan error, doclose bool) { + if doclose { + defer close(output) + defer close(status) + } + + input = NormalizePath(input) + log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor) + file, err := os.Open(input) + if err != nil { + log.Fatalf("Failed to open file %s%s%s: %s%+v%s", SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor) + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + instruction, err := ParseInstruction(line) + if err != nil { + log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor) + continue + } + log.Printf("Read instruction: %s", instruction.String()) + output <- instruction + } +} +func ReadFromArgs(output chan LinkInstruction, status chan error) { + defer close(output) + defer close(status) + + log.Printf("Reading input from args") + for _, arg := range os.Args[1:] { + instruction, err := ParseInstruction(arg) + if err != nil { + log.Printf("Error parsing arg: %s'%s'%s, error: %s%+v%s", SourceColor, arg, DefaultColor, ErrorColor, err, DefaultColor) + continue + } + output <- instruction + } +} +func ReadFromStdin(output chan LinkInstruction, status chan error) { + defer close(output) + defer close(status) + + log.Printf("Reading input from stdin") + + info, err := os.Stdin.Stat() + if err != nil { + log.Fatalf("Failed to stat stdin: %s%+v%s", ErrorColor, err, DefaultColor) + status <- err + return + } + if info.Mode()&os.ModeNamedPipe != 0 { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() + instruction, err := ParseInstruction(line) + if err != nil { + log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor) + continue + } + output <- instruction + } + if err := scanner.Err(); err != nil { + log.Fatalf("Error reading from stdin: %s%+v%s", ErrorColor, err, DefaultColor) + status <- err + return + } + } +} diff --git a/util.go b/util.go index 3282112..f237f8b 100644 --- a/util.go +++ b/util.go @@ -1,142 +1,142 @@ -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - "sync" - "sync/atomic" - "time" -) - -func IsSymlink(path string) (bool, error) { - fileInfo, err := os.Lstat(path) - if err != nil { - return false, err - } - - // os.ModeSymlink is a bitmask that identifies the symlink mode. - // If the file mode & os.ModeSymlink is non-zero, the file is a symlink. - return fileInfo.Mode()&os.ModeSymlink != 0, nil -} - -func FileExists(path string) bool { - _, err := os.Lstat(path) - return err == nil -} - -func NormalizePath(input string) string { - workingdirectory, _ := os.Getwd() - input = strings.ReplaceAll(input, "\\", "/") - input = strings.ReplaceAll(input, "\"", "") - - if !filepath.IsAbs(input) { - input = workingdirectory + "/" + input - } - - return filepath.Clean(input) -} - -func AreSame(lhs string, rhs string) bool { - lhsinfo, err := os.Stat(lhs) - if err != nil { - return false - } - rhsinfo, err := os.Stat(rhs) - if err != nil { - return false - } - - return os.SameFile(lhsinfo, rhsinfo) -} - -func ConvertHome(input string) (string, error) { - if strings.Contains(input, "~") { - homedir, err := os.UserHomeDir() - if err != nil { - return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err) - } - - return strings.Replace(input, "~", homedir, 1), nil - } - return input, nil -} - -func GetSyncFilesRecursively(input string, output chan string, status chan error) { - defer close(output) - defer close(status) - - var filesProcessed int32 - var foldersProcessed int32 - progressTicker := time.NewTicker(200 * time.Millisecond) - defer progressTicker.Stop() - - var wg sync.WaitGroup - var initial sync.Once - wg.Add(1) - directories := make(chan string, 100000) - workerPool := make(chan struct{}, 4000) - directories <- input - - go func() { - for { - fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", atomic.LoadInt32((&filesProcessed)), atomic.LoadInt32(&foldersProcessed), len(workerPool), len(directories)) - <-progressTicker.C - } - }() - - // log.Printf("%+v", len(workerPool)) - go func() { - for directory := range directories { - workerPool <- struct{}{} - wg.Add(1) - go func(directory string) { - atomic.AddInt32(&foldersProcessed, 1) - defer wg.Done() - defer func() { <-workerPool }() - - files, err := os.ReadDir(directory) - if err != nil { - log.Printf("Error reading directory %s: %+v", directory, err) - return - } - - for _, file := range files { - // log.Printf("Processing file %s", file.Name()) - if file.IsDir() { - directories <- filepath.Join(directory, file.Name()) - } else { - // log.Println(file.Name(), DirRegex.MatchString(file.Name())) - if FileRegex.MatchString(file.Name()) { - // log.Printf("Writing") - output <- filepath.Join(directory, file.Name()) - } - atomic.AddInt32(&filesProcessed, 1) - } - } - // log.Printf("Done reading directory %s", directory) - - initial.Do(func() { - // Parallelism is very difficult... - time.Sleep(250 * time.Millisecond) - wg.Done() - }) - }(directory) - } - }() - - // This actually does not go through ALL files sadly... - // It so happens (very often) that we manage to quit between one iteration ending - // And another beginning - // In such a state workgroup is decreased and, before it has a chance to increase, we are done - // What I should do here is only terminate if directories is empty - // ...but how do I do that? - // I might be wrong... Fuck knows... - // It also sometimes happens that wg.Wait triggers after wg.Done on line 97 but before the next (what would be!) wg.Add on line 94 - // This happens much more often with a small number of workers - // Such is the nature of race conditions... - wg.Wait() - log.Printf("Files processed: %d; Folders processed: %d", filesProcessed, foldersProcessed) -} +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "time" +) + +func IsSymlink(path string) (bool, error) { + fileInfo, err := os.Lstat(path) + if err != nil { + return false, err + } + + // os.ModeSymlink is a bitmask that identifies the symlink mode. + // If the file mode & os.ModeSymlink is non-zero, the file is a symlink. + return fileInfo.Mode()&os.ModeSymlink != 0, nil +} + +func FileExists(path string) bool { + _, err := os.Lstat(path) + return err == nil +} + +func NormalizePath(input string) string { + workingdirectory, _ := os.Getwd() + input = strings.ReplaceAll(input, "\\", "/") + input = strings.ReplaceAll(input, "\"", "") + + if !filepath.IsAbs(input) { + input = workingdirectory + "/" + input + } + + return filepath.Clean(input) +} + +func AreSame(lhs string, rhs string) bool { + lhsinfo, err := os.Stat(lhs) + if err != nil { + return false + } + rhsinfo, err := os.Stat(rhs) + if err != nil { + return false + } + + return os.SameFile(lhsinfo, rhsinfo) +} + +func ConvertHome(input string) (string, error) { + if strings.Contains(input, "~") { + homedir, err := os.UserHomeDir() + if err != nil { + return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err) + } + + return strings.Replace(input, "~", homedir, 1), nil + } + return input, nil +} + +func GetSyncFilesRecursively(input string, output chan string, status chan error) { + defer close(output) + defer close(status) + + var filesProcessed int32 + var foldersProcessed int32 + progressTicker := time.NewTicker(200 * time.Millisecond) + defer progressTicker.Stop() + + var wg sync.WaitGroup + var initial sync.Once + wg.Add(1) + directories := make(chan string, 100000) + workerPool := make(chan struct{}, 4000) + directories <- input + + go func() { + for { + fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", atomic.LoadInt32((&filesProcessed)), atomic.LoadInt32(&foldersProcessed), len(workerPool), len(directories)) + <-progressTicker.C + } + }() + + // log.Printf("%+v", len(workerPool)) + go func() { + for directory := range directories { + workerPool <- struct{}{} + wg.Add(1) + go func(directory string) { + atomic.AddInt32(&foldersProcessed, 1) + defer wg.Done() + defer func() { <-workerPool }() + + files, err := os.ReadDir(directory) + if err != nil { + log.Printf("Error reading directory %s: %+v", directory, err) + return + } + + for _, file := range files { + // log.Printf("Processing file %s", file.Name()) + if file.IsDir() { + directories <- filepath.Join(directory, file.Name()) + } else { + // log.Println(file.Name(), DirRegex.MatchString(file.Name())) + if FileRegex.MatchString(file.Name()) { + // log.Printf("Writing") + output <- filepath.Join(directory, file.Name()) + } + atomic.AddInt32(&filesProcessed, 1) + } + } + // log.Printf("Done reading directory %s", directory) + + initial.Do(func() { + // Parallelism is very difficult... + time.Sleep(250 * time.Millisecond) + wg.Done() + }) + }(directory) + } + }() + + // This actually does not go through ALL files sadly... + // It so happens (very often) that we manage to quit between one iteration ending + // And another beginning + // In such a state workgroup is decreased and, before it has a chance to increase, we are done + // What I should do here is only terminate if directories is empty + // ...but how do I do that? + // I might be wrong... Fuck knows... + // It also sometimes happens that wg.Wait triggers after wg.Done on line 97 but before the next (what would be!) wg.Add on line 94 + // This happens much more often with a small number of workers + // Such is the nature of race conditions... + wg.Wait() + log.Printf("Files processed: %d; Folders processed: %d", filesProcessed, foldersProcessed) +}