1 Commits

Author SHA1 Message Date
195c4ab3ad Add insane ramblings 2024-07-01 20:08:23 +02:00
9 changed files with 514 additions and 699 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,2 @@
*.exe
*.exe main.exe
cln

View File

@@ -1,35 +0,0 @@
# 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,99 +0,0 @@
package main
import (
"fmt"
"math/rand/v2"
)
const (
// Reset
Reset = "\033[0m" // Text Reset
// Regular Colors
Black = "\033[0;30m" // Black
Red = "\033[0;31m" // Red
Green = "\033[0;32m" // Green
Yellow = "\033[0;33m" // Yellow
Blue = "\033[0;34m" // Blue
Purple = "\033[0;35m" // Purple
Cyan = "\033[0;36m" // Cyan
White = "\033[0;37m" // White
// Bold
BBlack = "\033[1;30m" // Black
BRed = "\033[1;31m" // Red
BGreen = "\033[1;32m" // Green
BYellow = "\033[1;33m" // Yellow
BBlue = "\033[1;34m" // Blue
BPurple = "\033[1;35m" // Purple
BCyan = "\033[1;36m" // Cyan
BWhite = "\033[1;37m" // White
// Underline
UBlack = "\033[4;30m" // Black
URed = "\033[4;31m" // Red
UGreen = "\033[4;32m" // Green
UYellow = "\033[4;33m" // Yellow
UBlue = "\033[4;34m" // Blue
UPurple = "\033[4;35m" // Purple
UCyan = "\033[4;36m" // Cyan
UWhite = "\033[4;37m" // White
// Background
On_Black = "\033[40m" // Black
On_Red = "\033[41m" // Red
On_Green = "\033[42m" // Green
On_Yellow = "\033[43m" // Yellow
On_Blue = "\033[44m" // Blue
On_Purple = "\033[45m" // Purple
On_Cyan = "\033[46m" // Cyan
On_White = "\033[47m" // White
// High Intensty
IBlack = "\033[0;90m" // Black
IRed = "\033[0;91m" // Red
IGreen = "\033[0;92m" // Green
IYellow = "\033[0;93m" // Yellow
IBlue = "\033[0;94m" // Blue
IPurple = "\033[0;95m" // Purple
ICyan = "\033[0;96m" // Cyan
IWhite = "\033[0;97m" // White
// Bold High Intensty
BIBlack = "\033[1;90m" // Black
BIRed = "\033[1;91m" // Red
BIGreen = "\033[1;92m" // Green
BIYellow = "\033[1;93m" // Yellow
BIBlue = "\033[1;94m" // Blue
BIPurple = "\033[1;95m" // Purple
BICyan = "\033[1;96m" // Cyan
BIWhite = "\033[1;97m" // White
// High Intensty backgrounds
On_IBlack = "\033[0;100m" // Black
On_IRed = "\033[0;101m" // Red
On_IGreen = "\033[0;102m" // Green
On_IYellow = "\033[0;103m" // Yellow
On_IBlue = "\033[0;104m" // Blue
On_IPurple = "\033[10;95m" // Purple
On_ICyan = "\033[0;106m" // Cyan
On_IWhite = "\033[0;107m" // White
)
// The acceptable range is [16, 231] but here we remove some very dark colors
// That make text unreadable on a dark terminal
// See https://www.hackitu.de/termcolor256/
var colors = []int{22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 57, 62, 63, 64, 65, 67, 68, 69, 70, 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 148, 149, 150, 151, 152, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 184, 185, 186, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 226, 227, 228, 229, 230}
var colorsIndex int = -1
var shuffled bool
func GenerateRandomAnsiColor() string {
if !shuffled {
rand.Shuffle(len(colors), func(i int, j int) {
colors[i], colors[j] = colors[j], colors[i]
})
shuffled = true
}
colorsIndex++
return fmt.Sprintf("\033[1;4;38;5;%dm", colors[colorsIndex%len(colors)])
}

View File

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

2
go.mod
View File

@@ -1,3 +1,3 @@
module cln module main
go 1.21.7 go 1.21.7

View File

@@ -1,154 +1,132 @@
package main package main
import ( import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath" "regexp"
"regexp" "strconv"
"strconv" "strings"
"strings" )
)
type LinkInstruction struct {
type LinkInstruction struct { Source string
Source string Target string
Target string Force bool
Force bool }
Hard bool
Delete 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 (instruction *LinkInstruction) Tidy() {
instruction.Source = strings.ReplaceAll(instruction.Source, "\"", "") func ParseInstruction(line string) (LinkInstruction, error) {
instruction.Source = strings.ReplaceAll(instruction.Source, "\\", "/") parts := strings.Split(line, deliminer)
instruction.Source = strings.TrimSpace(instruction.Source) instruction := LinkInstruction{}
instruction.Target = strings.ReplaceAll(instruction.Target, "\"", "") if len(parts) < 2 {
instruction.Target = strings.ReplaceAll(instruction.Target, "\\", "/") return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)")
instruction.Target = strings.TrimSpace(instruction.Target) }
}
instruction.Source = parts[0]
func (instruction *LinkInstruction) String() string { instruction.Target = parts[1]
return fmt.Sprintf("%s%s%s%s%s%s%s%s%s%s%s%s%s", SourceColor, instruction.Source, DefaultColor, deliminer, TargetColor, instruction.Target, DefaultColor, deliminer, strconv.FormatBool(instruction.Force), deliminer, strconv.FormatBool(instruction.Hard), deliminer, strconv.FormatBool(instruction.Delete)) instruction.Force = false
} if len(parts) > 2 {
res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2])
func ParseInstruction(line, workdir string) (LinkInstruction, error) { instruction.Force = res
line = strings.TrimSpace(line) }
parts := strings.Split(line, deliminer)
instruction := LinkInstruction{} instruction.Source, _ = ConvertHome(instruction.Source)
instruction.Target, _ = ConvertHome(instruction.Target)
if len(parts) < 2 {
return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)") instruction.Source = NormalizePath(instruction.Source)
} instruction.Target = NormalizePath(instruction.Target)
instruction.Source = strings.TrimSpace(parts[0]) return instruction, nil
instruction.Target = strings.TrimSpace(parts[1]) }
instruction.Force = false
if len(parts) > 2 { func (instruction *LinkInstruction) RunSync() error {
res, _ := regexp.MatchString(`^(?i)\s*T|TRUE\s*$`, parts[2]) if !FileExists(instruction.Source) {
instruction.Force = res return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
} }
if len(parts) > 3 {
res, _ := regexp.MatchString(`^(?i)\s*T|TRUE\s*$`, parts[3]) if AreSame(instruction.Source, instruction.Target) {
instruction.Hard = res 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 len(parts) > 4 { }
res, _ := regexp.MatchString(`^(?i)\s*T|TRUE\s*$`, parts[4])
instruction.Delete = res if FileExists(instruction.Target) {
instruction.Force = res if instruction.Force {
} isSymlink, err := IsSymlink(instruction.Target)
if err != nil {
instruction.Tidy() 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)
instruction.Source, _ = ConvertHome(instruction.Source) }
instruction.Target, _ = ConvertHome(instruction.Target)
if isSymlink {
instruction.Source = NormalizePath(instruction.Source, workdir) log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
instruction.Target = NormalizePath(instruction.Target, workdir) err = os.Remove(instruction.Target)
if err != nil {
return instruction, nil return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
} }
} else {
func (instruction *LinkInstruction) RunAsync(status chan (error)) { return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
defer close(status) }
if !FileExists(instruction.Source) { } else {
status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor) return fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor)
return }
} }
if !instruction.Force && AreSame(instruction.Source, instruction.Target) { err := os.Symlink(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) if err != nil {
return 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)
if FileExists(instruction.Target) {
if instruction.Force { return nil
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) func (instruction *LinkInstruction) RunAsync(status chan (error)) {
return defer close(status)
} if !FileExists(instruction.Source) {
status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
if instruction.Hard { return
info, err := os.Stat(instruction.Target) }
if err != nil {
status <- fmt.Errorf("could not stat %s%s%s, stopping; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) if AreSame(instruction.Source, instruction.Target) {
return 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 info.Mode().IsRegular() && info.Name() == filepath.Base(instruction.Source) { }
log.Printf("Overwriting existing file %s%s%s", TargetColor, instruction.Target, DefaultColor)
err := os.Remove(instruction.Target) if FileExists(instruction.Target) {
if err != nil { if instruction.Force {
status <- fmt.Errorf("could not remove existing file %s%s%s; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) isSymlink, err := IsSymlink(instruction.Target)
return 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 { 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 {
if !instruction.Delete { 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 {
log.Printf("%sDeleting (!!!)%s %s%s%s", ImportantColor, DefaultColor, 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)
err = os.RemoveAll(instruction.Target) return
if err != nil { }
status <- fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor) }
return
} err := os.Symlink(instruction.Source, instruction.Target)
} if err != nil {
} else { 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("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor) return
return }
} log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
}
status <- nil
targetDir := filepath.Dir(instruction.Target) }
if _, err := os.Stat(targetDir); os.IsNotExist(err) {
err = os.MkdirAll(targetDir, 0755)
if err != nil {
status <- fmt.Errorf("failed creating directory %s%s%s due to %s%+v%s", TargetColor, targetDir, DefaultColor, ErrorColor, err, DefaultColor)
return
}
}
var err error
if instruction.Hard {
err = os.Link(instruction.Source, instruction.Target)
} else {
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
}

490
main.go
View File

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

2
sync
View File

@@ -1 +1 @@
foo,"~/bar" test,testdir/test3

291
util.go
View File

@@ -1,152 +1,139 @@
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, workdir string) string { func NormalizePath(input string) string {
input = filepath.Clean(input) workingdirectory, _ := os.Getwd()
input = filepath.ToSlash(input) input = strings.ReplaceAll(input, "\\", "/")
input = strings.ReplaceAll(input, "\"", "") input = strings.ReplaceAll(input, "\"", "")
if !filepath.IsAbs(input) { if !filepath.IsAbs(input) {
log.Printf("Input '%s' is not absolute, prepending work dir '%s'", input, workdir) input = workingdirectory + "/" + input
var err error }
input = filepath.Join(workdir, input)
input, err = filepath.Abs(input) return filepath.Clean(input)
if err != nil { }
log.Printf("Failed to get absolute path for %s%s%s: %s%+v%s", SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor)
return input func AreSame(lhs string, rhs string) bool {
} lhsinfo, err := os.Stat(lhs)
} if err != nil {
return false
input = filepath.Clean(input) }
input = filepath.ToSlash(input) rhsinfo, err := os.Stat(rhs)
return input if err != nil {
} return false
}
func AreSame(lhs string, rhs string) bool {
lhsinfo, err := os.Stat(lhs) return os.SameFile(lhsinfo, rhsinfo)
if err != nil { }
return false
} func ConvertHome(input string) (string, error) {
rhsinfo, err := os.Stat(rhs) if strings.Contains(input, "~") {
if err != nil { homedir, err := os.UserHomeDir()
return false if err != nil {
} return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err)
}
return os.SameFile(lhsinfo, rhsinfo)
} return strings.Replace(input, "~", homedir, 1), nil
}
func ConvertHome(input string) (string, error) { return input, nil
if strings.HasPrefix(input, "~/") { }
homedir, err := os.UserHomeDir()
if err != nil { func GetSyncFilesRecursively(input string, output chan string, status chan error) {
return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err) defer close(output)
} defer close(status)
return strings.Replace(input, "~", homedir, 1), nil var filesProcessed int32
} var foldersProcessed int32
return input, nil progressTicker := time.NewTicker(200 * time.Millisecond)
} defer progressTicker.Stop()
func GetSyncFilesRecursively(input string, output chan string, status chan error) { var wg sync.WaitGroup
defer close(output) var initial sync.Once
defer close(status) wg.Add(1)
directories := make(chan string, 100000)
var filesProcessed int32 workerPool := make(chan struct{}, 4000)
var foldersProcessed int32 directories <- input
progressTicker := time.NewTicker(200 * time.Millisecond)
defer progressTicker.Stop() go func() {
for {
var wg sync.WaitGroup fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", filesProcessed, foldersProcessed, len(workerPool), len(directories))
var initial sync.Once <-progressTicker.C
var done bool }
wg.Add(1) }()
directories := make(chan string, 100000)
workerPool := make(chan struct{}, 4000) log.Printf("%+v", len(workerPool))
directories <- input go func() {
for directory := range directories {
go func() { workerPool <- struct{}{}
for { wg.Add(1)
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", atomic.LoadInt32((&filesProcessed)), atomic.LoadInt32(&foldersProcessed), len(workerPool), len(directories)) go func(directory string) {
<-progressTicker.C atomic.AddInt32(&foldersProcessed, 1)
} defer wg.Done()
}() defer func() { <-workerPool }()
// log.Printf("%+v", len(workerPool)) files, err := os.ReadDir(directory)
go func() { if err != nil {
for directory := range directories { log.Printf("Error reading directory %s: %+v", directory, err)
workerPool <- struct{}{} return
wg.Add(1) }
go func(directory string) {
atomic.AddInt32(&foldersProcessed, 1) for _, file := range files {
defer wg.Done() // log.Printf("Processing file %s", file.Name())
defer func() { <-workerPool }() if file.IsDir() {
directories <- filepath.Join(directory, file.Name())
files, err := os.ReadDir(directory) } else {
if err != nil { // log.Println(file.Name(), DirRegex.MatchString(file.Name()))
log.Printf("Error reading directory %s: %+v", directory, err) if FileRegex.MatchString(file.Name()) {
return // log.Printf("Writing")
} output <- filepath.Join(directory, file.Name())
}
for _, file := range files { atomic.AddInt32(&filesProcessed, 1)
// log.Printf("Processing file %s", file.Name()) }
if file.IsDir() { }
directories <- filepath.Join(directory, file.Name()) // log.Printf("Done reading directory %s", directory)
} else {
// log.Println(file.Name(), DirRegex.MatchString(file.Name())) initial.Do(func() {
if FileRegex.MatchString(file.Name()) { // Parallelism is very difficult...
// log.Printf("Writing") time.Sleep(250 * time.Millisecond)
output <- filepath.Join(directory, file.Name()) wg.Done()
} })
atomic.AddInt32(&filesProcessed, 1) }(directory)
} }
} }()
// log.Printf("Done reading directory %s", directory)
done = len(directories) == 0 // This actually does not go through ALL files sadly...
if done { // It so happens (very often) that we manage to quit between one iteration ending
initial.Do(func() { // And another beginning
wg.Done() // 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?
}(directory) // I might be wrong... Fuck knows...
} 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)
}