27 Commits

Author SHA1 Message Date
1c23ad0cfd Fix issue with defaulting to reading from file while providing other args 2024-11-10 15:58:52 +01:00
653e883742 Fix issue with incorrectly parsing ~ 2024-10-18 20:19:31 +02:00
41bac18525 Replace ~ only if the path begins with ~ 2024-10-08 14:11:38 +02:00
02106824fd Discover go install 2024-10-03 10:53:16 +02:00
cd42bc1fb8 Fix issue with non absolute paths not being absoluted 2024-09-23 16:16:47 +02:00
41123846d1 Convert all paths to absolute
To fix issue where folders were being linked as files
2024-09-20 18:24:10 +02:00
205f8314d6 Improve filepath resolution for relative paths 2024-09-11 19:43:43 +02:00
290e6fdaba Now force forces creation 2024-09-11 19:37:07 +02:00
d98ecd787a Add colors and refine logs a little 2024-09-11 19:35:33 +02:00
1a6992e2a7 Remove sync 2024-09-11 19:32:02 +02:00
fc59878389 Implement deleting real actual files 2024-09-11 19:32:01 +02:00
ff1af19088 Ensure target exists by creating directories 2024-09-11 18:53:22 +02:00
e6bb1f0c53 Update 2024-08-16 14:57:20 +02:00
e149103d21 Print "hard" property on instruction 2024-08-16 09:46:15 +02:00
ebccc49d34 Make forced hard links overwrite target if it has the same name as source
Or something like that... Maybe...
2024-08-16 09:45:05 +02:00
55dc061c31 Default file to "sync" 2024-08-16 09:39:14 +02:00
2c49b65502 Move hard out of flag args and into command 2024-08-16 09:38:35 +02:00
1f21965288 Update sync file 2024-08-15 22:46:34 +02:00
b8e7bc3576 Add support for hard links 2024-08-15 22:44:03 +02:00
PhatPhuckDave
595a11552c Trim instruction before parse 2024-07-21 13:44:26 +02:00
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
9 changed files with 699 additions and 514 deletions

5
.gitignore vendored
View File

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

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

99
colors.go Normal file
View File

@@ -0,0 +1,99 @@
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 +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

2
go.mod
View File

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

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
@@ -13,13 +14,26 @@ type LinkInstruction struct {
Source string
Target string
Force bool
Hard bool
Delete bool
}
func (instruction *LinkInstruction) Tidy() {
instruction.Source = strings.ReplaceAll(instruction.Source, "\"", "")
instruction.Source = strings.ReplaceAll(instruction.Source, "\\", "/")
instruction.Source = strings.TrimSpace(instruction.Source)
instruction.Target = strings.ReplaceAll(instruction.Target, "\"", "")
instruction.Target = strings.ReplaceAll(instruction.Target, "\\", "/")
instruction.Target = strings.TrimSpace(instruction.Target)
}
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%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))
}
func ParseInstruction(line string) (LinkInstruction, error) {
func ParseInstruction(line, workdir string) (LinkInstruction, error) {
line = strings.TrimSpace(line)
parts := strings.Split(line, deliminer)
instruction := LinkInstruction{}
@@ -27,63 +41,33 @@ func ParseInstruction(line string) (LinkInstruction, error) {
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.Source = strings.TrimSpace(parts[0])
instruction.Target = strings.TrimSpace(parts[1])
instruction.Force = false
if len(parts) > 2 {
res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2])
res, _ := regexp.MatchString(`^(?i)\s*T|TRUE\s*$`, parts[2])
instruction.Force = res
}
if len(parts) > 3 {
res, _ := regexp.MatchString(`^(?i)\s*T|TRUE\s*$`, parts[3])
instruction.Hard = res
}
if len(parts) > 4 {
res, _ := regexp.MatchString(`^(?i)\s*T|TRUE\s*$`, parts[4])
instruction.Delete = res
instruction.Force = res
}
instruction.Tidy()
instruction.Source, _ = ConvertHome(instruction.Source)
instruction.Target, _ = ConvertHome(instruction.Target)
instruction.Source = NormalizePath(instruction.Source)
instruction.Target = NormalizePath(instruction.Target)
instruction.Source = NormalizePath(instruction.Source, workdir)
instruction.Target = NormalizePath(instruction.Target, workdir)
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) {
@@ -91,7 +75,7 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
return
}
if AreSame(instruction.Source, instruction.Target) {
if !instruction.Force && 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
}
@@ -104,6 +88,22 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
return
}
if instruction.Hard {
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)
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 err != nil {
status <- fmt.Errorf("could not remove existing file %s%s%s; 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)
@@ -112,16 +112,38 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
return
}
} else {
if !instruction.Delete {
status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
return
}
log.Printf("%sDeleting (!!!)%s %s%s%s", ImportantColor, DefaultColor, TargetColor, instruction.Target, DefaultColor)
err = os.RemoveAll(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("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)
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

110
main.go
View File

@@ -6,25 +6,17 @@ import (
"io"
"log"
"os"
"path/filepath"
"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 SourceColor = Purple
const TargetColor = Yellow
const ErrorColor = Red
const ErrorColor = URed
const ImportantColor = BRed
const DefaultColor = White
const PathColor = Green
@@ -51,19 +43,40 @@ func main() {
log.SetFlags(log.Lmicroseconds)
}
log.Printf("Recurse: %s", *recurse)
log.Printf("File: %s", *file)
instructions := make(chan LinkInstruction, 1000)
instructions := make(chan *LinkInstruction, 1000)
status := make(chan error)
if *recurse != "" {
// Check input sources in priority order
switch {
case *recurse != "":
log.Printf("Recurse: %s", *recurse)
go ReadFromFilesRecursively(*recurse, instructions, status)
} else if *file != "" {
case *file != "":
log.Printf("File: %s", *file)
go ReadFromFile(*file, instructions, status, true)
} else if len(os.Args) > 1 {
case len(flag.Args()) > 0:
log.Printf("Reading from command line arguments")
go ReadFromArgs(instructions, status)
} else {
case IsPipeInput():
log.Printf("Reading from stdin pipe")
go ReadFromStdin(instructions, status)
default:
if _, err := os.Stat("sync"); err == nil {
log.Printf("Using default sync file")
go ReadFromFile("sync", instructions, status, true)
} else {
log.Printf("No input provided")
log.Printf("Provide input as: ")
log.Printf("Arguments - %s <source>,<target>,<force?>", programName)
log.Printf("File - %s -f <file>", programName)
log.Printf("Folder (finding sync files in folder recursively) - %s -r <folder>", programName)
log.Printf("stdin - (cat <file> | %s)", programName)
os.Exit(1)
}
}
go func() {
@@ -100,21 +113,25 @@ func main() {
wg.Wait()
log.Println("All done")
if instructionsDone == 0 {
log.Printf("No input provided")
log.Printf("Provide input as: ")
log.Printf("Arguments - %s <source>,<target>,<force?>", programName)
log.Printf("File - %s -f <file>", programName)
log.Printf("Folder (finding sync files in folder recursively) - %s -r <folder>", programName)
log.Printf("stdin - (cat <file> | %s)", programName)
log.Printf("No instructions were processed")
os.Exit(1)
}
}
func ReadFromFilesRecursively(input string, output chan LinkInstruction, status chan error) {
func IsPipeInput() bool {
info, err := os.Stdin.Stat()
if err != nil {
return false
}
return info.Mode()&os.ModeNamedPipe != 0
}
func ReadFromFilesRecursively(input string, output chan *LinkInstruction, status chan error) {
defer close(output)
defer close(status)
input = NormalizePath(input)
workdir, _ := os.Getwd()
input = NormalizePath(input, workdir)
log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor)
files := make(chan string, 128)
@@ -144,7 +161,7 @@ func ReadFromFilesRecursively(input string, output chan LinkInstruction, status
go func() {
defer wg.Done()
log.Println(file)
file = NormalizePath(file)
file = NormalizePath(file, workdir)
log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor)
// This "has" to be done because instructions are resolved in relation to cwd
@@ -165,13 +182,14 @@ func ReadFromFilesRecursively(input string, output chan LinkInstruction, status
}
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 {
defer close(output)
defer close(status)
}
input = NormalizePath(input)
input = NormalizePath(input, filepath.Dir(input))
log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor)
file, err := os.Open(input)
if err != nil {
@@ -183,51 +201,48 @@ func ReadFromFile(input string, output chan LinkInstruction, status chan error,
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
instruction, err := ParseInstruction(line)
instruction, err := ParseInstruction(line, filepath.Dir(input))
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
output <- &instruction
}
}
func ReadFromArgs(output chan LinkInstruction, status chan error) {
func ReadFromArgs(output chan *LinkInstruction, status chan error) {
defer close(output)
defer close(status)
workdir, _ := os.Getwd()
log.Printf("Reading input from args")
for _, arg := range os.Args[1:] {
instruction, err := ParseInstruction(arg)
for _, arg := range flag.Args() {
instruction, err := ParseInstruction(arg, workdir)
if err != nil {
log.Printf("Error parsing arg: %s'%s'%s, error: %s%+v%s", SourceColor, arg, DefaultColor, ErrorColor, err, DefaultColor)
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(status)
workdir, _ := os.Getwd()
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)
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
output <- &instruction
}
if err := scanner.Err(); err != nil {
log.Fatalf("Error reading from stdin: %s%+v%s", ErrorColor, err, DefaultColor)
@@ -235,4 +250,3 @@ func ReadFromStdin(output chan LinkInstruction, status chan error) {
return
}
}
}

2
sync
View File

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

35
util.go
View File

@@ -27,16 +27,25 @@ func FileExists(path string) bool {
return err == nil
}
func NormalizePath(input string) string {
workingdirectory, _ := os.Getwd()
input = strings.ReplaceAll(input, "\\", "/")
func NormalizePath(input, workdir string) string {
input = filepath.Clean(input)
input = filepath.ToSlash(input)
input = strings.ReplaceAll(input, "\"", "")
if !filepath.IsAbs(input) {
input = workingdirectory + "/" + input
log.Printf("Input '%s' is not absolute, prepending work dir '%s'", input, workdir)
var err error
input = filepath.Join(workdir, input)
input, err = filepath.Abs(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
}
}
return filepath.Clean(input)
input = filepath.Clean(input)
input = filepath.ToSlash(input)
return input
}
func AreSame(lhs string, rhs string) bool {
@@ -53,7 +62,7 @@ func AreSame(lhs string, rhs string) bool {
}
func ConvertHome(input string) (string, error) {
if strings.Contains(input, "~") {
if strings.HasPrefix(input, "~/") {
homedir, err := os.UserHomeDir()
if err != nil {
return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err)
@@ -75,6 +84,7 @@ func GetSyncFilesRecursively(input string, output chan string, status chan error
var wg sync.WaitGroup
var initial sync.Once
var done bool
wg.Add(1)
directories := make(chan string, 100000)
workerPool := make(chan struct{}, 4000)
@@ -82,12 +92,12 @@ func GetSyncFilesRecursively(input string, output chan string, status chan error
go func() {
for {
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", filesProcessed, foldersProcessed, len(workerPool), len(directories))
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))
// log.Printf("%+v", len(workerPool))
go func() {
for directory := range directories {
workerPool <- struct{}{}
@@ -117,12 +127,12 @@ func GetSyncFilesRecursively(input string, output chan string, status chan error
}
}
// log.Printf("Done reading directory %s", directory)
done = len(directories) == 0
if done {
initial.Do(func() {
// Parallelism is very difficult...
time.Sleep(250 * time.Millisecond)
wg.Done()
})
}
}(directory)
}
}()
@@ -134,6 +144,9 @@ func GetSyncFilesRecursively(input string, output chan string, status chan error
// 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)
}