Compare commits
31 Commits
dev
...
83477d5f18
Author | SHA1 | Date | |
---|---|---|---|
83477d5f18 | |||
125bf78c16 | |||
edaa699c20 | |||
5b2da09eb2 | |||
1c23ad0cfd | |||
653e883742 | |||
41bac18525 | |||
02106824fd | |||
cd42bc1fb8 | |||
41123846d1 | |||
205f8314d6 | |||
290e6fdaba | |||
d98ecd787a | |||
1a6992e2a7 | |||
fc59878389 | |||
ff1af19088 | |||
e6bb1f0c53 | |||
e149103d21 | |||
ebccc49d34 | |||
55dc061c31 | |||
2c49b65502 | |||
1f21965288 | |||
b8e7bc3576 | |||
![]() |
595a11552c | ||
2a7740d8d7 | |||
7580ca5399 | |||
58cce74ce8 | |||
e022a838ba | |||
0a627ae9ca | |||
d72644aec3 | |||
eeb8dac3a0 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
|
*.exe
|
||||||
main.exe
|
*.exe
|
||||||
|
cln
|
||||||
|
89
README.md
Normal file
89
README.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# synclib
|
||||||
|
|
||||||
|
A small Go tool for creating symbolic links
|
||||||
|
|
||||||
|
Created out of infuriating difficulty of creating symbolic links on windows
|
||||||
|
|
||||||
|
## Instruction Formats
|
||||||
|
|
||||||
|
The tool supports two formats for defining symbolic links:
|
||||||
|
|
||||||
|
### 1. CSV Format (Legacy)
|
||||||
|
|
||||||
|
Simple comma-separated values with the format: `<source>,<destination>[,force][,hard][,delete]`
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
source_path,target_path
|
||||||
|
source_path,target_path,true
|
||||||
|
source_path,target_path,true,true
|
||||||
|
source_path,target_path,true,true,true
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with named flags:
|
||||||
|
```
|
||||||
|
source_path,target_path,force=true,hard=true,delete=true
|
||||||
|
source_path,target_path,f=true,h=true,d=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. YAML Format (Recommended)
|
||||||
|
|
||||||
|
A more readable format using YAML:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
links:
|
||||||
|
- source: ~/Documents/config.ini
|
||||||
|
target: ~/.config/app/config.ini
|
||||||
|
force: true
|
||||||
|
|
||||||
|
- source: ~/Pictures
|
||||||
|
target: ~/Documents/Pictures
|
||||||
|
hard: true
|
||||||
|
force: true
|
||||||
|
|
||||||
|
- source: ~/Scripts/script.sh
|
||||||
|
target: ~/bin/script.sh
|
||||||
|
delete: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can use an array directly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- source: ~/Documents/config.ini
|
||||||
|
target: ~/.config/app/config.ini
|
||||||
|
force: true
|
||||||
|
|
||||||
|
- source: ~/Pictures
|
||||||
|
target: ~/Documents/Pictures
|
||||||
|
hard: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Methods
|
||||||
|
|
||||||
|
The tool 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>` (CSV format)
|
||||||
|
- `sync -f <file.yaml>` or `sync -f <file.yml>` (YAML format)
|
||||||
|
- Where the file contains instructions, one per line for CSV or structured YAML
|
||||||
|
- Directories
|
||||||
|
- `sync -r <directory>`
|
||||||
|
- This mode will look for "sync", "sync.yaml", or "sync.yml" files recursively in directories and run their instructions
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
- `force: true` - Overwrite an existing symbolic link at the target location
|
||||||
|
- `hard: true` - Create a hard link instead of a symbolic link
|
||||||
|
- `delete: true` - Delete a non-symlink file at the target location (implies `force: true`)
|
||||||
|
|
||||||
|
## 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
99
colors.go
Normal 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)])
|
||||||
|
}
|
@@ -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
|
4
go.mod
4
go.mod
@@ -1,3 +1,5 @@
|
|||||||
module main
|
module cln
|
||||||
|
|
||||||
go 1.21.7
|
go 1.21.7
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
387
instruction.go
387
instruction.go
@@ -1,132 +1,255 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"path/filepath"
|
||||||
"strconv"
|
"strings"
|
||||||
"strings"
|
|
||||||
)
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
type LinkInstruction struct {
|
|
||||||
Source string
|
type LinkInstruction struct {
|
||||||
Target string
|
Source string `yaml:"source"`
|
||||||
Force bool
|
Target string `yaml:"target"`
|
||||||
}
|
Force bool `yaml:"force,omitempty"`
|
||||||
|
Hard bool `yaml:"hard,omitempty"`
|
||||||
func (instruction *LinkInstruction) String() string {
|
Delete bool `yaml:"delete,omitempty"`
|
||||||
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))
|
}
|
||||||
}
|
|
||||||
|
type YAMLConfig struct {
|
||||||
func ParseInstruction(line string) (LinkInstruction, error) {
|
Links []LinkInstruction `yaml:"links"`
|
||||||
parts := strings.Split(line, deliminer)
|
}
|
||||||
instruction := LinkInstruction{}
|
|
||||||
|
func (instruction *LinkInstruction) Tidy() {
|
||||||
if len(parts) < 2 {
|
instruction.Source = strings.ReplaceAll(instruction.Source, "\"", "")
|
||||||
return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)")
|
instruction.Source = strings.ReplaceAll(instruction.Source, "\\", "/")
|
||||||
}
|
instruction.Source = strings.TrimSpace(instruction.Source)
|
||||||
|
|
||||||
instruction.Source = parts[0]
|
instruction.Target = strings.ReplaceAll(instruction.Target, "\"", "")
|
||||||
instruction.Target = parts[1]
|
instruction.Target = strings.ReplaceAll(instruction.Target, "\\", "/")
|
||||||
instruction.Force = false
|
instruction.Target = strings.TrimSpace(instruction.Target)
|
||||||
if len(parts) > 2 {
|
}
|
||||||
res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2])
|
|
||||||
instruction.Force = res
|
func (instruction *LinkInstruction) String() string {
|
||||||
}
|
var flags []string
|
||||||
|
if instruction.Force {
|
||||||
instruction.Source, _ = ConvertHome(instruction.Source)
|
flags = append(flags, "force=true")
|
||||||
instruction.Target, _ = ConvertHome(instruction.Target)
|
}
|
||||||
|
if instruction.Hard {
|
||||||
instruction.Source = NormalizePath(instruction.Source)
|
flags = append(flags, "hard=true")
|
||||||
instruction.Target = NormalizePath(instruction.Target)
|
}
|
||||||
|
if instruction.Delete {
|
||||||
return instruction, nil
|
flags = append(flags, "delete=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instruction *LinkInstruction) RunSync() error {
|
flagsStr := ""
|
||||||
if !FileExists(instruction.Source) {
|
if len(flags) > 0 {
|
||||||
return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
|
flagsStr = " [" + strings.Join(flags, ", ") + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
if AreSame(instruction.Source, instruction.Target) {
|
return fmt.Sprintf("%s%s%s → %s%s%s%s",
|
||||||
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)
|
SourceColor, instruction.Source, DefaultColor,
|
||||||
return nil
|
TargetColor, instruction.Target, DefaultColor,
|
||||||
}
|
flagsStr)
|
||||||
|
}
|
||||||
if FileExists(instruction.Target) {
|
|
||||||
if instruction.Force {
|
func ParseInstruction(line, workdir string) (LinkInstruction, error) {
|
||||||
isSymlink, err := IsSymlink(instruction.Target)
|
line = strings.TrimSpace(line)
|
||||||
if err != nil {
|
if strings.HasPrefix(line, "#") {
|
||||||
return fmt.Errorf("could not determine whether %s%s%s is a sym link or not, stopping; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
|
return LinkInstruction{}, fmt.Errorf("comment line")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSymlink {
|
parts := strings.Split(line, deliminer)
|
||||||
log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
instruction := LinkInstruction{}
|
||||||
err = os.Remove(instruction.Target)
|
|
||||||
if err != nil {
|
if len(parts) < 2 {
|
||||||
return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
|
return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
instruction.Source = strings.TrimSpace(parts[0])
|
||||||
}
|
instruction.Target = strings.TrimSpace(parts[1])
|
||||||
} else {
|
|
||||||
return fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor)
|
for i := 2; i < len(parts); i++ {
|
||||||
}
|
flagPart := strings.TrimSpace(parts[i])
|
||||||
}
|
|
||||||
|
// Support for legacy format (backward compatibility)
|
||||||
err := os.Symlink(instruction.Source, instruction.Target)
|
if !strings.Contains(flagPart, "=") {
|
||||||
if err != nil {
|
// Legacy format: positional boolean flags
|
||||||
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)
|
switch i {
|
||||||
}
|
case 2: // Force flag (3rd position)
|
||||||
log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
|
instruction.Force = isTrue(flagPart)
|
||||||
|
case 3: // Hard flag (4th position)
|
||||||
return nil
|
instruction.Hard = isTrue(flagPart)
|
||||||
}
|
case 4: // Delete flag (5th position)
|
||||||
|
instruction.Delete = isTrue(flagPart)
|
||||||
func (instruction *LinkInstruction) RunAsync(status chan (error)) {
|
if instruction.Delete {
|
||||||
defer close(status)
|
instruction.Force = true // Delete implies Force
|
||||||
if !FileExists(instruction.Source) {
|
}
|
||||||
status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
|
}
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if AreSame(instruction.Source, instruction.Target) {
|
// New format: named flags (name=value)
|
||||||
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)
|
nameValue := strings.SplitN(flagPart, "=", 2)
|
||||||
return
|
if len(nameValue) != 2 {
|
||||||
}
|
// Skip malformed flags
|
||||||
|
continue
|
||||||
if FileExists(instruction.Target) {
|
}
|
||||||
if instruction.Force {
|
|
||||||
isSymlink, err := IsSymlink(instruction.Target)
|
flagName := strings.ToLower(strings.TrimSpace(nameValue[0]))
|
||||||
if err != nil {
|
flagValue := strings.TrimSpace(nameValue[1])
|
||||||
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
|
switch flagName {
|
||||||
}
|
case "force", "f":
|
||||||
|
instruction.Force = isTrue(flagValue)
|
||||||
if isSymlink {
|
case "hard", "h":
|
||||||
log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
instruction.Hard = isTrue(flagValue)
|
||||||
err = os.Remove(instruction.Target)
|
case "delete", "d":
|
||||||
if err != nil {
|
instruction.Delete = isTrue(flagValue)
|
||||||
status <- fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
|
if instruction.Delete {
|
||||||
return
|
instruction.Force = true // Delete implies Force
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
}
|
||||||
return
|
|
||||||
}
|
instruction.Tidy()
|
||||||
} else {
|
instruction.Source, _ = ConvertHome(instruction.Source)
|
||||||
status <- fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor)
|
instruction.Target, _ = ConvertHome(instruction.Target)
|
||||||
return
|
|
||||||
}
|
instruction.Source = NormalizePath(instruction.Source, workdir)
|
||||||
}
|
instruction.Target = NormalizePath(instruction.Target, workdir)
|
||||||
|
|
||||||
err := os.Symlink(instruction.Source, instruction.Target)
|
return instruction, nil
|
||||||
if err != nil {
|
}
|
||||||
status <- fmt.Errorf("failed creating symlink between %s%s%s and %s%s%s with error %s%+v%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
|
|
||||||
return
|
func isTrue(value string) bool {
|
||||||
}
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
|
return value == "true" || value == "t" || value == "yes" || value == "y" || value == "1"
|
||||||
|
}
|
||||||
status <- 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 !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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseYAMLFile(filename, workdir string) ([]LinkInstruction, error) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading YAML file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try to parse as a list of link instructions
|
||||||
|
var config YAMLConfig
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil || len(config.Links) == 0 {
|
||||||
|
// If that fails, try parsing as a direct list of instructions
|
||||||
|
var instructions []LinkInstruction
|
||||||
|
err = yaml.Unmarshal(data, &instructions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing YAML: %w", err)
|
||||||
|
}
|
||||||
|
config.Links = instructions
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range config.Links {
|
||||||
|
config.Links[i].Tidy()
|
||||||
|
config.Links[i].Source, _ = ConvertHome(config.Links[i].Source)
|
||||||
|
config.Links[i].Target, _ = ConvertHome(config.Links[i].Target)
|
||||||
|
config.Links[i].Source = NormalizePath(config.Links[i].Source, workdir)
|
||||||
|
config.Links[i].Target = NormalizePath(config.Links[i].Target, workdir)
|
||||||
|
|
||||||
|
// If Delete is true, Force must also be true
|
||||||
|
if config.Links[i].Delete {
|
||||||
|
config.Links[i].Force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.Links, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsYAMLFile(filename string) bool {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
return ext == ".yaml" || ext == ".yml"
|
||||||
|
}
|
||||||
|
520
main.go
520
main.go
@@ -1,238 +1,282 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"path/filepath"
|
||||||
"sync"
|
"regexp"
|
||||||
"sync/atomic"
|
"sync"
|
||||||
)
|
"sync/atomic"
|
||||||
|
)
|
||||||
const deliminer = ","
|
|
||||||
const (
|
const deliminer = ","
|
||||||
Black = "\033[30m"
|
const SourceColor = Purple
|
||||||
Red = "\033[31m"
|
const TargetColor = Yellow
|
||||||
Green = "\033[32m"
|
const ErrorColor = URed
|
||||||
Yellow = "\033[33m"
|
const ImportantColor = BRed
|
||||||
Blue = "\033[34m"
|
const DefaultColor = White
|
||||||
Magenta = "\033[35m"
|
const PathColor = Green
|
||||||
Cyan = "\033[36m"
|
|
||||||
White = "\033[37m"
|
var DirRegex, _ = regexp.Compile(`^(.+?)[/\\]sync(?:\.ya?ml)?$`)
|
||||||
)
|
var FileRegex, _ = regexp.Compile(`^sync(?:\.ya?ml)?$`)
|
||||||
const SourceColor = Magenta
|
var programName = os.Args[0]
|
||||||
const TargetColor = Yellow
|
|
||||||
const ErrorColor = Red
|
func main() {
|
||||||
const DefaultColor = White
|
recurse := flag.String("r", "", "recurse into directories")
|
||||||
const PathColor = Green
|
file := flag.String("f", "", "file to read instructions from")
|
||||||
|
debug := flag.Bool("d", false, "debug")
|
||||||
var DirRegex, _ = regexp.Compile(`^(.+?)[/\\]sync$`)
|
flag.Parse()
|
||||||
var FileRegex, _ = regexp.Compile(`^sync$`)
|
|
||||||
var programName = os.Args[0]
|
if *debug {
|
||||||
|
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
||||||
func main() {
|
logFile, err := os.Create("main.log")
|
||||||
recurse := flag.String("r", "", "recurse into directories")
|
if err != nil {
|
||||||
file := flag.String("f", "", "file to read instructions from")
|
log.Printf("Error creating log file: %v", err)
|
||||||
debug := flag.Bool("d", false, "debug")
|
os.Exit(1)
|
||||||
flag.Parse()
|
}
|
||||||
|
logger := io.MultiWriter(os.Stdout, logFile)
|
||||||
if *debug {
|
log.SetOutput(logger)
|
||||||
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
} else {
|
||||||
logFile, err := os.Create("main.log")
|
log.SetFlags(log.Lmicroseconds)
|
||||||
if err != nil {
|
}
|
||||||
log.Printf("Error creating log file: %v", err)
|
|
||||||
os.Exit(1)
|
instructions := make(chan *LinkInstruction, 1000)
|
||||||
}
|
status := make(chan error)
|
||||||
logger := io.MultiWriter(os.Stdout, logFile)
|
|
||||||
log.SetOutput(logger)
|
// Check input sources in priority order
|
||||||
} else {
|
switch {
|
||||||
log.SetFlags(log.Lmicroseconds)
|
case *recurse != "":
|
||||||
}
|
log.Printf("Recurse: %s", *recurse)
|
||||||
|
go ReadFromFilesRecursively(*recurse, instructions, status)
|
||||||
log.Printf("Recurse: %s", *recurse)
|
|
||||||
log.Printf("File: %s", *file)
|
case *file != "":
|
||||||
|
log.Printf("File: %s", *file)
|
||||||
instructions := make(chan LinkInstruction, 1000)
|
go ReadFromFile(*file, instructions, status, true)
|
||||||
status := make(chan error)
|
|
||||||
if *recurse != "" {
|
case len(flag.Args()) > 0:
|
||||||
go ReadFromFilesRecursively(*recurse, instructions, status)
|
log.Printf("Reading from command line arguments")
|
||||||
} else if *file != "" {
|
go ReadFromArgs(instructions, status)
|
||||||
go ReadFromFile(*file, instructions, status, true)
|
|
||||||
} else if len(os.Args) > 1 {
|
case IsPipeInput():
|
||||||
go ReadFromArgs(instructions, status)
|
log.Printf("Reading from stdin pipe")
|
||||||
} else {
|
go ReadFromStdin(instructions, status)
|
||||||
go ReadFromStdin(instructions, status)
|
|
||||||
}
|
default:
|
||||||
|
if _, err := os.Stat("sync"); err == nil {
|
||||||
go func() {
|
log.Printf("Using default sync file")
|
||||||
for {
|
go ReadFromFile("sync", instructions, status, true)
|
||||||
err, ok := <-status
|
} else if _, err := os.Stat("sync.yaml"); err == nil {
|
||||||
if !ok {
|
log.Printf("Using default sync.yaml file")
|
||||||
break
|
go ReadFromFile("sync.yaml", instructions, status, true)
|
||||||
}
|
} else if _, err := os.Stat("sync.yml"); err == nil {
|
||||||
if err != nil {
|
log.Printf("Using default sync.yml file")
|
||||||
log.Println(err)
|
go ReadFromFile("sync.yml", instructions, status, true)
|
||||||
}
|
} else {
|
||||||
}
|
log.Printf("No input provided")
|
||||||
}()
|
log.Printf("Provide input as: ")
|
||||||
|
log.Printf("Arguments - %s <source>,<target>,<force?>", programName)
|
||||||
var instructionsDone int32
|
log.Printf("File - %s -f <file>", programName)
|
||||||
var wg sync.WaitGroup
|
log.Printf("YAML File - %s -f <file.yaml>", programName)
|
||||||
for {
|
log.Printf("Folder (finding sync files in folder recursively) - %s -r <folder>", programName)
|
||||||
instruction, ok := <-instructions
|
log.Printf("stdin - (cat <file> | %s)", programName)
|
||||||
if !ok {
|
os.Exit(1)
|
||||||
log.Printf("No more instructions to process")
|
}
|
||||||
break
|
}
|
||||||
}
|
|
||||||
log.Printf("Processing: %s", instruction.String())
|
go func() {
|
||||||
status := make(chan error)
|
for {
|
||||||
go instruction.RunAsync(status)
|
err, ok := <-status
|
||||||
wg.Add(1)
|
if !ok {
|
||||||
err := <-status
|
break
|
||||||
if err != nil {
|
}
|
||||||
log.Printf("Failed parsing instruction %s%s%s due to %s%+v%s", SourceColor, instruction.String(), DefaultColor, ErrorColor, err, DefaultColor)
|
if err != nil {
|
||||||
}
|
log.Println(err)
|
||||||
atomic.AddInt32(&instructionsDone, 1)
|
}
|
||||||
wg.Done()
|
}
|
||||||
}
|
}()
|
||||||
wg.Wait()
|
|
||||||
log.Println("All done")
|
var instructionsDone int32
|
||||||
if instructionsDone == 0 {
|
var wg sync.WaitGroup
|
||||||
log.Printf("No input provided")
|
for {
|
||||||
log.Printf("Provide input as: ")
|
instruction, ok := <-instructions
|
||||||
log.Printf("Arguments - %s <source>,<target>,<force?>", programName)
|
if !ok {
|
||||||
log.Printf("File - %s -f <file>", programName)
|
log.Printf("No more instructions to process")
|
||||||
log.Printf("Folder (finding sync files in folder recursively) - %s -r <folder>", programName)
|
break
|
||||||
log.Printf("stdin - (cat <file> | %s)", programName)
|
}
|
||||||
os.Exit(1)
|
log.Printf("Processing: %s", instruction.String())
|
||||||
}
|
status := make(chan error)
|
||||||
}
|
go instruction.RunAsync(status)
|
||||||
|
wg.Add(1)
|
||||||
func ReadFromFilesRecursively(input string, output chan LinkInstruction, status chan error) {
|
err := <-status
|
||||||
defer close(output)
|
if err != nil {
|
||||||
defer close(status)
|
log.Printf("Failed parsing instruction %s%s%s due to %s%+v%s", SourceColor, instruction.String(), DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
|
}
|
||||||
input = NormalizePath(input)
|
atomic.AddInt32(&instructionsDone, 1)
|
||||||
log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor)
|
wg.Done()
|
||||||
|
}
|
||||||
files := make(chan string, 128)
|
wg.Wait()
|
||||||
recurseStatus := make(chan error)
|
log.Println("All done")
|
||||||
go GetSyncFilesRecursively(input, files, recurseStatus)
|
if instructionsDone == 0 {
|
||||||
go func() {
|
log.Printf("No instructions were processed")
|
||||||
for {
|
os.Exit(1)
|
||||||
err, ok := <-recurseStatus
|
}
|
||||||
if !ok {
|
}
|
||||||
break
|
|
||||||
}
|
func IsPipeInput() bool {
|
||||||
if err != nil {
|
info, err := os.Stdin.Stat()
|
||||||
log.Printf("Failed to get sync files recursively: %s%+v%s", ErrorColor, err, DefaultColor)
|
if err != nil {
|
||||||
status <- err
|
return false
|
||||||
}
|
}
|
||||||
}
|
return info.Mode()&os.ModeNamedPipe != 0
|
||||||
}()
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
func ReadFromFilesRecursively(input string, output chan *LinkInstruction, status chan error) {
|
||||||
for {
|
defer close(output)
|
||||||
file, ok := <-files
|
defer close(status)
|
||||||
if !ok {
|
|
||||||
log.Printf("No more files to process")
|
workdir, _ := os.Getwd()
|
||||||
break
|
input = NormalizePath(input, workdir)
|
||||||
}
|
log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor)
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
files := make(chan string, 128)
|
||||||
defer wg.Done()
|
recurseStatus := make(chan error)
|
||||||
log.Println(file)
|
go GetSyncFilesRecursively(input, files, recurseStatus)
|
||||||
file = NormalizePath(file)
|
go func() {
|
||||||
log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor)
|
for {
|
||||||
|
err, ok := <-recurseStatus
|
||||||
// This "has" to be done because instructions are resolved in relation to cwd
|
if !ok {
|
||||||
fileDir := DirRegex.FindStringSubmatch(file)
|
break
|
||||||
if fileDir == nil {
|
}
|
||||||
log.Printf("Failed to extract directory from %s%s%s", SourceColor, file, DefaultColor)
|
if err != nil {
|
||||||
return
|
log.Printf("Failed to get sync files recursively: %s%+v%s", ErrorColor, err, DefaultColor)
|
||||||
}
|
status <- err
|
||||||
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
|
var wg sync.WaitGroup
|
||||||
}
|
for {
|
||||||
|
file, ok := <-files
|
||||||
ReadFromFile(file, output, status, false)
|
if !ok {
|
||||||
}()
|
log.Printf("No more files to process")
|
||||||
}
|
break
|
||||||
wg.Wait()
|
}
|
||||||
}
|
wg.Add(1)
|
||||||
func ReadFromFile(input string, output chan LinkInstruction, status chan error, doclose bool) {
|
go func() {
|
||||||
if doclose {
|
defer wg.Done()
|
||||||
defer close(output)
|
log.Println(file)
|
||||||
defer close(status)
|
file = NormalizePath(file, workdir)
|
||||||
}
|
log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor)
|
||||||
|
|
||||||
input = NormalizePath(input)
|
// This "has" to be done because instructions are resolved in relation to cwd
|
||||||
log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor)
|
fileDir := DirRegex.FindStringSubmatch(file)
|
||||||
file, err := os.Open(input)
|
if fileDir == nil {
|
||||||
if err != nil {
|
log.Printf("Failed to extract directory from %s%s%s", SourceColor, file, DefaultColor)
|
||||||
log.Fatalf("Failed to open file %s%s%s: %s%+v%s", SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor)
|
return
|
||||||
return
|
}
|
||||||
}
|
log.Printf("Changing directory to %s%s%s (for %s%s%s)", PathColor, fileDir[1], DefaultColor, PathColor, file, DefaultColor)
|
||||||
defer file.Close()
|
err := os.Chdir(fileDir[1])
|
||||||
|
if err != nil {
|
||||||
scanner := bufio.NewScanner(file)
|
log.Printf("Failed to change directory to %s%s%s: %s%+v%s", SourceColor, fileDir[1], DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
for scanner.Scan() {
|
return
|
||||||
line := scanner.Text()
|
}
|
||||||
instruction, err := ParseInstruction(line)
|
|
||||||
if err != nil {
|
ReadFromFile(file, output, status, false)
|
||||||
log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor)
|
}()
|
||||||
continue
|
}
|
||||||
}
|
wg.Wait()
|
||||||
log.Printf("Read instruction: %s", instruction.String())
|
}
|
||||||
output <- instruction
|
|
||||||
}
|
func ReadFromFile(input string, output chan *LinkInstruction, status chan error, doclose bool) {
|
||||||
}
|
if doclose {
|
||||||
func ReadFromArgs(output chan LinkInstruction, status chan error) {
|
defer close(output)
|
||||||
defer close(output)
|
defer close(status)
|
||||||
defer close(status)
|
}
|
||||||
|
|
||||||
log.Printf("Reading input from args")
|
input = NormalizePath(input, filepath.Dir(input))
|
||||||
for _, arg := range os.Args[1:] {
|
log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor)
|
||||||
instruction, err := ParseInstruction(arg)
|
|
||||||
if err != nil {
|
// Check if this is a YAML file
|
||||||
log.Printf("Error parsing arg: %s'%s'%s, error: %s%+v%s", SourceColor, arg, DefaultColor, ErrorColor, err, DefaultColor)
|
if IsYAMLFile(input) {
|
||||||
continue
|
log.Printf("Parsing as YAML file")
|
||||||
}
|
instructions, err := ParseYAMLFile(input, filepath.Dir(input))
|
||||||
output <- instruction
|
if err != nil {
|
||||||
}
|
log.Printf("Failed to parse YAML file %s%s%s: %s%+v%s",
|
||||||
}
|
SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
func ReadFromStdin(output chan LinkInstruction, status chan error) {
|
status <- err
|
||||||
defer close(output)
|
return
|
||||||
defer close(status)
|
}
|
||||||
|
|
||||||
log.Printf("Reading input from stdin")
|
for _, instruction := range instructions {
|
||||||
|
instr := instruction // Create a copy to avoid reference issues
|
||||||
info, err := os.Stdin.Stat()
|
log.Printf("Read YAML instruction: %s", instr.String())
|
||||||
if err != nil {
|
output <- &instr
|
||||||
log.Fatalf("Failed to stat stdin: %s%+v%s", ErrorColor, err, DefaultColor)
|
}
|
||||||
status <- err
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
if info.Mode()&os.ModeNamedPipe != 0 {
|
// Handle CSV format (legacy)
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
file, err := os.Open(input)
|
||||||
for scanner.Scan() {
|
if err != nil {
|
||||||
line := scanner.Text()
|
log.Fatalf("Failed to open file %s%s%s: %s%+v%s",
|
||||||
instruction, err := ParseInstruction(line)
|
SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
if err != nil {
|
return
|
||||||
log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor)
|
}
|
||||||
continue
|
defer file.Close()
|
||||||
}
|
|
||||||
output <- instruction
|
scanner := bufio.NewScanner(file)
|
||||||
}
|
for scanner.Scan() {
|
||||||
if err := scanner.Err(); err != nil {
|
line := scanner.Text()
|
||||||
log.Fatalf("Error reading from stdin: %s%+v%s", ErrorColor, err, DefaultColor)
|
instruction, err := ParseInstruction(line, filepath.Dir(input))
|
||||||
status <- err
|
if err != nil {
|
||||||
return
|
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)
|
||||||
|
|
||||||
|
workdir, _ := os.Getwd()
|
||||||
|
log.Printf("Reading input from args")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFromStdin(output chan *LinkInstruction, status chan error) {
|
||||||
|
defer close(output)
|
||||||
|
defer close(status)
|
||||||
|
|
||||||
|
workdir, _ := os.Getwd()
|
||||||
|
log.Printf("Reading input from stdin")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
10
sync.yaml
Normal file
10
sync.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
- source: main.go
|
||||||
|
target: test/main.go
|
||||||
|
|
||||||
|
- source: README.md
|
||||||
|
target: test/README.md
|
||||||
|
|
||||||
|
- source: sync.yaml
|
||||||
|
target: test/sync.yaml
|
||||||
|
|
||||||
|
|
26
sync.yaml.example
Normal file
26
sync.yaml.example
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Example sync.yaml file
|
||||||
|
# You can use this format to define symbolic links
|
||||||
|
# Each link specifies source, target, and optional flags
|
||||||
|
links:
|
||||||
|
- source: ~/Documents/config.ini
|
||||||
|
target: ~/.config/app/config.ini
|
||||||
|
# This will create a symbolic link, overwriting any existing symlink
|
||||||
|
force: true
|
||||||
|
|
||||||
|
- source: ~/Pictures
|
||||||
|
target: ~/Documents/Pictures
|
||||||
|
# This will create a hard link instead of a symbolic link
|
||||||
|
hard: true
|
||||||
|
force: true
|
||||||
|
|
||||||
|
- source: ~/Scripts/script.sh
|
||||||
|
target: ~/bin/script.sh
|
||||||
|
# This will delete a non-symlink file at the target location
|
||||||
|
# 'delete: true' implies 'force: true'
|
||||||
|
delete: true
|
||||||
|
|
||||||
|
# Alternative format:
|
||||||
|
# Instead of using the 'links' property, you can define an array directly:
|
||||||
|
# - source: ~/Documents/config.ini
|
||||||
|
# target: ~/.config/app/config.ini
|
||||||
|
# force: true
|
316
util.go
316
util.go
@@ -1,132 +1,184 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsSymlink(path string) (bool, error) {
|
func IsSymlink(path string) (bool, error) {
|
||||||
fileInfo, err := os.Lstat(path)
|
fileInfo, err := os.Lstat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// os.ModeSymlink is a bitmask that identifies the symlink mode.
|
// os.ModeSymlink is a bitmask that identifies the symlink mode.
|
||||||
// If the file mode & os.ModeSymlink is non-zero, the file is a symlink.
|
// If the file mode & os.ModeSymlink is non-zero, the file is a symlink.
|
||||||
return fileInfo.Mode()&os.ModeSymlink != 0, nil
|
return fileInfo.Mode()&os.ModeSymlink != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileExists(path string) bool {
|
func FileExists(path string) bool {
|
||||||
_, err := os.Lstat(path)
|
_, err := os.Lstat(path)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NormalizePath(input string) string {
|
func NormalizePath(input, workdir string) string {
|
||||||
workingdirectory, _ := os.Getwd()
|
input = filepath.Clean(input)
|
||||||
input = strings.ReplaceAll(input, "\\", "/")
|
input = filepath.ToSlash(input)
|
||||||
input = strings.ReplaceAll(input, "\"", "")
|
input = strings.ReplaceAll(input, "\"", "")
|
||||||
|
|
||||||
if !filepath.IsAbs(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)
|
||||||
return filepath.Clean(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)
|
||||||
func AreSame(lhs string, rhs string) bool {
|
return input
|
||||||
lhsinfo, err := os.Stat(lhs)
|
}
|
||||||
if err != nil {
|
}
|
||||||
return false
|
|
||||||
}
|
input = filepath.Clean(input)
|
||||||
rhsinfo, err := os.Stat(rhs)
|
input = filepath.ToSlash(input)
|
||||||
if err != nil {
|
return input
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
|
func AreSame(lhs string, rhs string) bool {
|
||||||
return os.SameFile(lhsinfo, rhsinfo)
|
lhsinfo, err := os.Stat(lhs)
|
||||||
}
|
if err != nil {
|
||||||
|
return false
|
||||||
func ConvertHome(input string) (string, error) {
|
}
|
||||||
if strings.Contains(input, "~") {
|
rhsinfo, err := os.Stat(rhs)
|
||||||
homedir, err := os.UserHomeDir()
|
if err != nil {
|
||||||
if err != nil {
|
return false
|
||||||
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
|
}
|
||||||
}
|
|
||||||
return input, nil
|
func ConvertHome(input string) (string, error) {
|
||||||
}
|
if strings.HasPrefix(input, "~/") {
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
func GetSyncFilesRecursively(input string, output chan string, status chan error) {
|
if err != nil {
|
||||||
defer close(output)
|
return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err)
|
||||||
defer close(status)
|
}
|
||||||
|
|
||||||
var filesProcessed int32
|
return strings.Replace(input, "~", homedir, 1), nil
|
||||||
var foldersProcessed int32
|
}
|
||||||
progressTicker := time.NewTicker(200 * time.Millisecond)
|
return input, nil
|
||||||
defer progressTicker.Stop()
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
func GetSyncFilesRecursively(input string, output chan string, status chan error) {
|
||||||
var initial sync.Once
|
defer close(output)
|
||||||
wg.Add(1)
|
defer close(status)
|
||||||
directories := make(chan string, 100000)
|
|
||||||
workerPool := make(chan struct{}, 4000)
|
var filesProcessed int32
|
||||||
directories <- input
|
var foldersProcessed int32
|
||||||
|
var activeWorkers int32
|
||||||
go func() {
|
|
||||||
for {
|
progressTicker := time.NewTicker(200 * time.Millisecond)
|
||||||
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", filesProcessed, foldersProcessed, len(workerPool), len(directories))
|
defer progressTicker.Stop()
|
||||||
<-progressTicker.C
|
|
||||||
}
|
done := make(chan struct{})
|
||||||
}()
|
defer close(done)
|
||||||
|
|
||||||
log.Printf("%+v", len(workerPool))
|
directories := make(chan string, 100000)
|
||||||
go func() {
|
workerPool := make(chan struct{}, 4000)
|
||||||
for directory := range directories {
|
directories <- input
|
||||||
workerPool <- struct{}{}
|
|
||||||
wg.Add(1)
|
go func() {
|
||||||
go func(directory string) {
|
for {
|
||||||
atomic.AddInt32(&foldersProcessed, 1)
|
select {
|
||||||
defer wg.Done()
|
case <-progressTicker.C:
|
||||||
defer func() { <-workerPool }()
|
dirCount := len(directories)
|
||||||
|
workers := atomic.LoadInt32(&activeWorkers)
|
||||||
files, err := os.ReadDir(directory)
|
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Active workers: %d; Directory queue: %d",
|
||||||
if err != nil {
|
atomic.LoadInt32(&filesProcessed),
|
||||||
log.Printf("Error reading directory %s: %+v", directory, err)
|
atomic.LoadInt32(&foldersProcessed),
|
||||||
return
|
workers,
|
||||||
}
|
dirCount)
|
||||||
|
case <-done:
|
||||||
for _, file := range files {
|
// Final progress update
|
||||||
// log.Printf("Processing file %s", file.Name())
|
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Completed successfully\n",
|
||||||
if file.IsDir() {
|
atomic.LoadInt32(&filesProcessed),
|
||||||
directories <- filepath.Join(directory, file.Name())
|
atomic.LoadInt32(&foldersProcessed))
|
||||||
} else {
|
return
|
||||||
// log.Println(file.Name(), DirRegex.MatchString(file.Name()))
|
}
|
||||||
if FileRegex.MatchString(file.Name()) {
|
}
|
||||||
// log.Printf("Writing")
|
}()
|
||||||
output <- filepath.Join(directory, file.Name())
|
|
||||||
}
|
allDone := make(chan struct{})
|
||||||
atomic.AddInt32(&filesProcessed, 1)
|
|
||||||
}
|
go func() {
|
||||||
}
|
var wg sync.WaitGroup
|
||||||
// log.Printf("Done reading directory %s", directory)
|
|
||||||
|
go func() {
|
||||||
initial.Do(func() {
|
for {
|
||||||
// Parallelism is very difficult...
|
if atomic.LoadInt32(&activeWorkers) == 0 && len(directories) == 0 {
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
wg.Done()
|
if atomic.LoadInt32(&activeWorkers) == 0 && len(directories) == 0 {
|
||||||
})
|
close(allDone)
|
||||||
}(directory)
|
return
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
wg.Wait()
|
}
|
||||||
log.Printf("Files processed: %d; Folders processed: %d", filesProcessed, foldersProcessed)
|
}()
|
||||||
}
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case directory, ok := <-directories:
|
||||||
|
if !ok {
|
||||||
|
wg.Wait()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt32(&activeWorkers, 1)
|
||||||
|
|
||||||
|
go func(dir string) {
|
||||||
|
workerPool <- struct{}{}
|
||||||
|
|
||||||
|
atomic.AddInt32(&foldersProcessed, 1)
|
||||||
|
processDirectory(dir, directories, output, &filesProcessed)
|
||||||
|
|
||||||
|
<-workerPool
|
||||||
|
atomic.AddInt32(&activeWorkers, -1)
|
||||||
|
}(directory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-allDone
|
||||||
|
|
||||||
|
log.Printf("Files processed: %d; Folders processed: %d",
|
||||||
|
atomic.LoadInt32(&filesProcessed),
|
||||||
|
atomic.LoadInt32(&foldersProcessed))
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDirectory(directory string, directories chan<- string, output chan<- string, filesProcessed *int32) {
|
||||||
|
files, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading directory %s: %+v", directory, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
directories <- filepath.Join(directory, file.Name())
|
||||||
|
} else {
|
||||||
|
if FileRegex.MatchString(file.Name()) || IsYAMLSyncFile(file.Name()) {
|
||||||
|
output <- filepath.Join(directory, file.Name())
|
||||||
|
}
|
||||||
|
atomic.AddInt32(filesProcessed, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsYAMLSyncFile(filename string) bool {
|
||||||
|
return filename == "sync.yaml" || filename == "sync.yml"
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user