Compare commits
7 Commits
e812db0dec
...
2a7740d8d7
Author | SHA1 | Date | |
---|---|---|---|
2a7740d8d7 | |||
7580ca5399 | |||
58cce74ce8 | |||
e022a838ba | |||
0a627ae9ca | |||
d72644aec3 | |||
eeb8dac3a0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
|
||||
main.exe
|
||||
main_linux
|
||||
|
35
README.md
Normal file
35
README.md
Normal 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
|
@@ -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
|
264
instruction.go
264
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
476
main.go
476
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 <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)
|
||||
}
|
||||
}
|
||||
|
||||
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 <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)
|
||||
}
|
||||
}
|
||||
|
||||
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 flag.Args() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
275
util.go
275
util.go
@@ -1,132 +1,143 @@
|
||||
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;", filesProcessed, 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)
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
var done bool
|
||||
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)
|
||||
done = len(directories) == 0
|
||||
if done {
|
||||
initial.Do(func() {
|
||||
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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user