Compare commits

...

7 Commits

Author SHA1 Message Date
2a7740d8d7 Refactor reading args 2024-07-03 13:31:12 +02:00
7580ca5399 Enable build for linux 2024-07-03 13:31:03 +02:00
58cce74ce8 Remove escapes from code "blocks" 2024-07-01 21:06:01 +02:00
e022a838ba Solve the race condition when recursively reading files, hopefully 2024-07-01 21:00:38 +02:00
0a627ae9ca Add readme 2024-07-01 20:38:38 +02:00
d72644aec3 Code format
I think? Don't know what changed
2024-07-01 20:30:19 +02:00
eeb8dac3a0 Add insane ramblings 2024-07-01 20:28:49 +02:00
6 changed files with 551 additions and 503 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
main.exe main.exe
main_linux

35
README.md Normal file
View File

@@ -0,0 +1,35 @@
# synclib
A small Go tool for creating symbolic links
Created out of infuriating difficulty of creating symbolic links on windows
## Custom syntax
The tool works with "instructions" that describe symbolic links
They are, in any form, \<source>,\<destination>,\<force?>
For example:
`sync this,that`
It supports input of these instructions through:
- Stdin
- `echo "this,that" | sync`
- Run arguments
- `sync this,that foo,bar "foo 2","C:/bar"`
- Files
- `sync -f <file>`
- Where the file contains instructions, one instruction per line
- Directories
- `sync -r <directory>`
- This mode will look for "sync" files recursively in directories and run their instructions
## Use case
I have a lot of folders (documents, projects, configurations) backed up via Seafile and to have the software using those folders find them at their usual location I'm creating soft symbolic links from the seafile drive to their original location
It would be problematic to have to redo all (or some part) of these symlinks when reinstalling the OS or having something somewhere explode (say software uninstalled) so I have all the instructions in sync files in individual folders in the seafile drive
Which means I can easily back up my configuration and `sync -r ~/Seafile` to symlink it where it belongs

View File

@@ -1 +1,2 @@
go build main && cp main.exe "/c/Program Files/Git/usr/bin/cln.exe" GOOS=windows GOARCH=amd64 go build -o main.exe main && cp main.exe "/c/Program Files/Git/usr/bin/cln.exe"
GOOS=linux GOARCH=amd64 go build -o main_linux main

View File

@@ -1,132 +1,132 @@
package main package main
import ( import (
"fmt" "fmt"
"log" "log"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
) )
type LinkInstruction struct { type LinkInstruction struct {
Source string Source string
Target string Target string
Force bool Force bool
} }
func (instruction *LinkInstruction) String() string { 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)) 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) { func ParseInstruction(line string) (LinkInstruction, error) {
parts := strings.Split(line, deliminer) parts := strings.Split(line, deliminer)
instruction := LinkInstruction{} instruction := LinkInstruction{}
if len(parts) < 2 { if len(parts) < 2 {
return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)") return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)")
} }
instruction.Source = parts[0] instruction.Source = parts[0]
instruction.Target = parts[1] instruction.Target = parts[1]
instruction.Force = false instruction.Force = false
if len(parts) > 2 { if len(parts) > 2 {
res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2]) res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2])
instruction.Force = res instruction.Force = res
} }
instruction.Source, _ = ConvertHome(instruction.Source) instruction.Source, _ = ConvertHome(instruction.Source)
instruction.Target, _ = ConvertHome(instruction.Target) instruction.Target, _ = ConvertHome(instruction.Target)
instruction.Source = NormalizePath(instruction.Source) instruction.Source = NormalizePath(instruction.Source)
instruction.Target = NormalizePath(instruction.Target) instruction.Target = NormalizePath(instruction.Target)
return instruction, nil return instruction, nil
} }
func (instruction *LinkInstruction) RunSync() error { func (instruction *LinkInstruction) RunSync() error {
if !FileExists(instruction.Source) { if !FileExists(instruction.Source) {
return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor) return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
} }
if AreSame(instruction.Source, instruction.Target) { 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) 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 return nil
} }
if FileExists(instruction.Target) { if FileExists(instruction.Target) {
if instruction.Force { if instruction.Force {
isSymlink, err := IsSymlink(instruction.Target) isSymlink, err := IsSymlink(instruction.Target)
if err != nil { 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) 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 { if isSymlink {
log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor) log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
err = os.Remove(instruction.Target) err = os.Remove(instruction.Target)
if err != nil { if err != nil {
return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
} }
} else { } else {
return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor) return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
} }
} else { } else {
return fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor) 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) err := os.Symlink(instruction.Source, instruction.Target)
if err != nil { 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) 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) log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
return nil return nil
} }
func (instruction *LinkInstruction) RunAsync(status chan (error)) { func (instruction *LinkInstruction) RunAsync(status chan (error)) {
defer close(status) defer close(status)
if !FileExists(instruction.Source) { if !FileExists(instruction.Source) {
status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor) status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
return return
} }
if AreSame(instruction.Source, instruction.Target) { 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) 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 return
} }
if FileExists(instruction.Target) { if FileExists(instruction.Target) {
if instruction.Force { if instruction.Force {
isSymlink, err := IsSymlink(instruction.Target) isSymlink, err := IsSymlink(instruction.Target)
if err != nil { 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) 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 return
} }
if isSymlink { if isSymlink {
log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor) log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
err = os.Remove(instruction.Target) err = os.Remove(instruction.Target)
if err != nil { if err != nil {
status <- fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) status <- fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
return return
} }
} else { } else {
status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor) status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
return return
} }
} else { } else {
status <- fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor) status <- fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor)
return return
} }
} }
err := os.Symlink(instruction.Source, instruction.Target) err := os.Symlink(instruction.Source, instruction.Target)
if err != nil { 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) 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 return
} }
log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor) log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
status <- nil status <- nil
} }

476
main.go
View File

@@ -1,238 +1,238 @@
package main package main
import ( import (
"bufio" "bufio"
"flag" "flag"
"io" "io"
"log" "log"
"os" "os"
"regexp" "regexp"
"sync" "sync"
"sync/atomic" "sync/atomic"
) )
const deliminer = "," const deliminer = ","
const ( const (
Black = "\033[30m" Black = "\033[30m"
Red = "\033[31m" Red = "\033[31m"
Green = "\033[32m" Green = "\033[32m"
Yellow = "\033[33m" Yellow = "\033[33m"
Blue = "\033[34m" Blue = "\033[34m"
Magenta = "\033[35m" Magenta = "\033[35m"
Cyan = "\033[36m" Cyan = "\033[36m"
White = "\033[37m" White = "\033[37m"
) )
const SourceColor = Magenta const SourceColor = Magenta
const TargetColor = Yellow const TargetColor = Yellow
const ErrorColor = Red const ErrorColor = Red
const DefaultColor = White const DefaultColor = White
const PathColor = Green const PathColor = Green
var DirRegex, _ = regexp.Compile(`^(.+?)[/\\]sync$`) var DirRegex, _ = regexp.Compile(`^(.+?)[/\\]sync$`)
var FileRegex, _ = regexp.Compile(`^sync$`) var FileRegex, _ = regexp.Compile(`^sync$`)
var programName = os.Args[0] var programName = os.Args[0]
func main() { func main() {
recurse := flag.String("r", "", "recurse into directories") recurse := flag.String("r", "", "recurse into directories")
file := flag.String("f", "", "file to read instructions from") file := flag.String("f", "", "file to read instructions from")
debug := flag.Bool("d", false, "debug") debug := flag.Bool("d", false, "debug")
flag.Parse() flag.Parse()
if *debug { if *debug {
log.SetFlags(log.Lmicroseconds | log.Lshortfile) log.SetFlags(log.Lmicroseconds | log.Lshortfile)
logFile, err := os.Create("main.log") logFile, err := os.Create("main.log")
if err != nil { if err != nil {
log.Printf("Error creating log file: %v", err) log.Printf("Error creating log file: %v", err)
os.Exit(1) os.Exit(1)
} }
logger := io.MultiWriter(os.Stdout, logFile) logger := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(logger) log.SetOutput(logger)
} else { } else {
log.SetFlags(log.Lmicroseconds) log.SetFlags(log.Lmicroseconds)
} }
log.Printf("Recurse: %s", *recurse) log.Printf("Recurse: %s", *recurse)
log.Printf("File: %s", *file) log.Printf("File: %s", *file)
instructions := make(chan LinkInstruction, 1000) instructions := make(chan LinkInstruction, 1000)
status := make(chan error) status := make(chan error)
if *recurse != "" { if *recurse != "" {
go ReadFromFilesRecursively(*recurse, instructions, status) go ReadFromFilesRecursively(*recurse, instructions, status)
} else if *file != "" { } else if *file != "" {
go ReadFromFile(*file, instructions, status, true) go ReadFromFile(*file, instructions, status, true)
} else if len(os.Args) > 1 { } else if len(os.Args) > 1 {
go ReadFromArgs(instructions, status) go ReadFromArgs(instructions, status)
} else { } else {
go ReadFromStdin(instructions, status) go ReadFromStdin(instructions, status)
} }
go func() { go func() {
for { for {
err, ok := <-status err, ok := <-status
if !ok { if !ok {
break break
} }
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
} }
}() }()
var instructionsDone int32 var instructionsDone int32
var wg sync.WaitGroup var wg sync.WaitGroup
for { for {
instruction, ok := <-instructions instruction, ok := <-instructions
if !ok { if !ok {
log.Printf("No more instructions to process") log.Printf("No more instructions to process")
break break
} }
log.Printf("Processing: %s", instruction.String()) log.Printf("Processing: %s", instruction.String())
status := make(chan error) status := make(chan error)
go instruction.RunAsync(status) go instruction.RunAsync(status)
wg.Add(1) wg.Add(1)
err := <-status err := <-status
if err != nil { if err != nil {
log.Printf("Failed parsing instruction %s%s%s due to %s%+v%s", SourceColor, instruction.String(), DefaultColor, ErrorColor, err, DefaultColor) log.Printf("Failed parsing instruction %s%s%s due to %s%+v%s", SourceColor, instruction.String(), DefaultColor, ErrorColor, err, DefaultColor)
} }
atomic.AddInt32(&instructionsDone, 1) atomic.AddInt32(&instructionsDone, 1)
wg.Done() wg.Done()
} }
wg.Wait() wg.Wait()
log.Println("All done") log.Println("All done")
if instructionsDone == 0 { if instructionsDone == 0 {
log.Printf("No input provided") log.Printf("No input provided")
log.Printf("Provide input as: ") log.Printf("Provide input as: ")
log.Printf("Arguments - %s <source>,<target>,<force?>", programName) log.Printf("Arguments - %s <source>,<target>,<force?>", programName)
log.Printf("File - %s -f <file>", programName) log.Printf("File - %s -f <file>", programName)
log.Printf("Folder (finding sync files in folder recursively) - %s -r <folder>", programName) log.Printf("Folder (finding sync files in folder recursively) - %s -r <folder>", programName)
log.Printf("stdin - (cat <file> | %s)", programName) log.Printf("stdin - (cat <file> | %s)", programName)
os.Exit(1) os.Exit(1)
} }
} }
func ReadFromFilesRecursively(input string, output chan LinkInstruction, status chan error) { func ReadFromFilesRecursively(input string, output chan LinkInstruction, status chan error) {
defer close(output) defer close(output)
defer close(status) defer close(status)
input = NormalizePath(input) input = NormalizePath(input)
log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor) log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor)
files := make(chan string, 128) files := make(chan string, 128)
recurseStatus := make(chan error) recurseStatus := make(chan error)
go GetSyncFilesRecursively(input, files, recurseStatus) go GetSyncFilesRecursively(input, files, recurseStatus)
go func() { go func() {
for { for {
err, ok := <-recurseStatus err, ok := <-recurseStatus
if !ok { if !ok {
break break
} }
if err != nil { if err != nil {
log.Printf("Failed to get sync files recursively: %s%+v%s", ErrorColor, err, DefaultColor) log.Printf("Failed to get sync files recursively: %s%+v%s", ErrorColor, err, DefaultColor)
status <- err status <- err
} }
} }
}() }()
var wg sync.WaitGroup var wg sync.WaitGroup
for { for {
file, ok := <-files file, ok := <-files
if !ok { if !ok {
log.Printf("No more files to process") log.Printf("No more files to process")
break break
} }
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
log.Println(file) log.Println(file)
file = NormalizePath(file) file = NormalizePath(file)
log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor) log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor)
// This "has" to be done because instructions are resolved in relation to cwd // This "has" to be done because instructions are resolved in relation to cwd
fileDir := DirRegex.FindStringSubmatch(file) fileDir := DirRegex.FindStringSubmatch(file)
if fileDir == nil { if fileDir == nil {
log.Printf("Failed to extract directory from %s%s%s", SourceColor, file, DefaultColor) log.Printf("Failed to extract directory from %s%s%s", SourceColor, file, DefaultColor)
return return
} }
log.Printf("Changing directory to %s%s%s (for %s%s%s)", PathColor, fileDir[1], DefaultColor, PathColor, file, DefaultColor) log.Printf("Changing directory to %s%s%s (for %s%s%s)", PathColor, fileDir[1], DefaultColor, PathColor, file, DefaultColor)
err := os.Chdir(fileDir[1]) err := os.Chdir(fileDir[1])
if err != nil { if err != nil {
log.Printf("Failed to change directory to %s%s%s: %s%+v%s", SourceColor, fileDir[1], DefaultColor, ErrorColor, err, DefaultColor) log.Printf("Failed to change directory to %s%s%s: %s%+v%s", SourceColor, fileDir[1], DefaultColor, ErrorColor, err, DefaultColor)
return return
} }
ReadFromFile(file, output, status, false) ReadFromFile(file, output, status, false)
}() }()
} }
wg.Wait() wg.Wait()
} }
func ReadFromFile(input string, output chan LinkInstruction, status chan error, doclose bool) { func ReadFromFile(input string, output chan LinkInstruction, status chan error, doclose bool) {
if doclose { if doclose {
defer close(output) defer close(output)
defer close(status) defer close(status)
} }
input = NormalizePath(input) input = NormalizePath(input)
log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor) log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor)
file, err := os.Open(input) file, err := os.Open(input)
if err != nil { if err != nil {
log.Fatalf("Failed to open file %s%s%s: %s%+v%s", SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor) log.Fatalf("Failed to open file %s%s%s: %s%+v%s", SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor)
return return
} }
defer file.Close() defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
instruction, err := ParseInstruction(line) instruction, err := ParseInstruction(line)
if err != nil { if err != nil {
log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor) log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor)
continue continue
} }
log.Printf("Read instruction: %s", instruction.String()) log.Printf("Read instruction: %s", instruction.String())
output <- instruction output <- instruction
} }
} }
func ReadFromArgs(output chan LinkInstruction, status chan error) { func ReadFromArgs(output chan LinkInstruction, status chan error) {
defer close(output) defer close(output)
defer close(status) defer close(status)
log.Printf("Reading input from args") log.Printf("Reading input from args")
for _, arg := range os.Args[1:] { for _, arg := range flag.Args() {
instruction, err := ParseInstruction(arg) instruction, err := ParseInstruction(arg)
if err != nil { if err != nil {
log.Printf("Error parsing arg: %s'%s'%s, error: %s%+v%s", SourceColor, arg, DefaultColor, ErrorColor, err, DefaultColor) log.Printf("Error parsing arg: %s'%s'%s, error: %s%+v%s", SourceColor, arg, DefaultColor, ErrorColor, err, DefaultColor)
continue continue
} }
output <- instruction output <- instruction
} }
} }
func ReadFromStdin(output chan LinkInstruction, status chan error) { func ReadFromStdin(output chan LinkInstruction, status chan error) {
defer close(output) defer close(output)
defer close(status) defer close(status)
log.Printf("Reading input from stdin") log.Printf("Reading input from stdin")
info, err := os.Stdin.Stat() info, err := os.Stdin.Stat()
if err != nil { if err != nil {
log.Fatalf("Failed to stat stdin: %s%+v%s", ErrorColor, err, DefaultColor) log.Fatalf("Failed to stat stdin: %s%+v%s", ErrorColor, err, DefaultColor)
status <- err status <- err
return return
} }
if info.Mode()&os.ModeNamedPipe != 0 { if info.Mode()&os.ModeNamedPipe != 0 {
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
instruction, err := ParseInstruction(line) instruction, err := ParseInstruction(line)
if err != nil { if err != nil {
log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor) log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor)
continue continue
} }
output <- instruction output <- instruction
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Fatalf("Error reading from stdin: %s%+v%s", ErrorColor, err, DefaultColor) log.Fatalf("Error reading from stdin: %s%+v%s", ErrorColor, err, DefaultColor)
status <- err status <- err
return return
} }
} }
} }

275
util.go
View File

@@ -1,132 +1,143 @@
package main package main
import ( import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
) )
func IsSymlink(path string) (bool, error) { func IsSymlink(path string) (bool, error) {
fileInfo, err := os.Lstat(path) fileInfo, err := os.Lstat(path)
if err != nil { if err != nil {
return false, err return false, err
} }
// os.ModeSymlink is a bitmask that identifies the symlink mode. // os.ModeSymlink is a bitmask that identifies the symlink mode.
// If the file mode & os.ModeSymlink is non-zero, the file is a symlink. // If the file mode & os.ModeSymlink is non-zero, the file is a symlink.
return fileInfo.Mode()&os.ModeSymlink != 0, nil return fileInfo.Mode()&os.ModeSymlink != 0, nil
} }
func FileExists(path string) bool { func FileExists(path string) bool {
_, err := os.Lstat(path) _, err := os.Lstat(path)
return err == nil return err == nil
} }
func NormalizePath(input string) string { func NormalizePath(input string) string {
workingdirectory, _ := os.Getwd() workingdirectory, _ := os.Getwd()
input = strings.ReplaceAll(input, "\\", "/") input = strings.ReplaceAll(input, "\\", "/")
input = strings.ReplaceAll(input, "\"", "") input = strings.ReplaceAll(input, "\"", "")
if !filepath.IsAbs(input) { if !filepath.IsAbs(input) {
input = workingdirectory + "/" + input input = workingdirectory + "/" + input
} }
return filepath.Clean(input) return filepath.Clean(input)
} }
func AreSame(lhs string, rhs string) bool { func AreSame(lhs string, rhs string) bool {
lhsinfo, err := os.Stat(lhs) lhsinfo, err := os.Stat(lhs)
if err != nil { if err != nil {
return false return false
} }
rhsinfo, err := os.Stat(rhs) rhsinfo, err := os.Stat(rhs)
if err != nil { if err != nil {
return false return false
} }
return os.SameFile(lhsinfo, rhsinfo) return os.SameFile(lhsinfo, rhsinfo)
} }
func ConvertHome(input string) (string, error) { func ConvertHome(input string) (string, error) {
if strings.Contains(input, "~") { if strings.Contains(input, "~") {
homedir, err := os.UserHomeDir() homedir, err := os.UserHomeDir()
if err != nil { if err != nil {
return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err) return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err)
} }
return strings.Replace(input, "~", homedir, 1), nil return strings.Replace(input, "~", homedir, 1), nil
} }
return input, nil return input, nil
} }
func GetSyncFilesRecursively(input string, output chan string, status chan error) { func GetSyncFilesRecursively(input string, output chan string, status chan error) {
defer close(output) defer close(output)
defer close(status) defer close(status)
var filesProcessed int32 var filesProcessed int32
var foldersProcessed int32 var foldersProcessed int32
progressTicker := time.NewTicker(200 * time.Millisecond) progressTicker := time.NewTicker(200 * time.Millisecond)
defer progressTicker.Stop() defer progressTicker.Stop()
var wg sync.WaitGroup var wg sync.WaitGroup
var initial sync.Once var initial sync.Once
wg.Add(1) var done bool
directories := make(chan string, 100000) wg.Add(1)
workerPool := make(chan struct{}, 4000) directories := make(chan string, 100000)
directories <- input workerPool := make(chan struct{}, 4000)
directories <- input
go func() {
for { go func() {
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", filesProcessed, foldersProcessed, len(workerPool), len(directories)) for {
<-progressTicker.C 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() { // log.Printf("%+v", len(workerPool))
for directory := range directories { go func() {
workerPool <- struct{}{} for directory := range directories {
wg.Add(1) workerPool <- struct{}{}
go func(directory string) { wg.Add(1)
atomic.AddInt32(&foldersProcessed, 1) go func(directory string) {
defer wg.Done() atomic.AddInt32(&foldersProcessed, 1)
defer func() { <-workerPool }() defer wg.Done()
defer func() { <-workerPool }()
files, err := os.ReadDir(directory)
if err != nil { files, err := os.ReadDir(directory)
log.Printf("Error reading directory %s: %+v", directory, err) if err != nil {
return log.Printf("Error reading directory %s: %+v", directory, err)
} return
}
for _, file := range files {
// log.Printf("Processing file %s", file.Name()) for _, file := range files {
if file.IsDir() { // log.Printf("Processing file %s", file.Name())
directories <- filepath.Join(directory, file.Name()) if file.IsDir() {
} else { directories <- filepath.Join(directory, file.Name())
// log.Println(file.Name(), DirRegex.MatchString(file.Name())) } else {
if FileRegex.MatchString(file.Name()) { // log.Println(file.Name(), DirRegex.MatchString(file.Name()))
// log.Printf("Writing") if FileRegex.MatchString(file.Name()) {
output <- filepath.Join(directory, file.Name()) // log.Printf("Writing")
} output <- filepath.Join(directory, file.Name())
atomic.AddInt32(&filesProcessed, 1) }
} atomic.AddInt32(&filesProcessed, 1)
} }
// log.Printf("Done reading directory %s", directory) }
// log.Printf("Done reading directory %s", directory)
initial.Do(func() { done = len(directories) == 0
// Parallelism is very difficult... if done {
time.Sleep(250 * time.Millisecond) initial.Do(func() {
wg.Done() wg.Done()
}) })
}(directory) }
} }(directory)
}() }
}()
wg.Wait()
log.Printf("Files processed: %d; Folders processed: %d", filesProcessed, foldersProcessed) // 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)
}