Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 195c4ab3ad |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,8 +1,2 @@
|
|||||||
*.exe
|
|
||||||
*.exe
|
main.exe
|
||||||
cln
|
|
||||||
cln.log
|
|
||||||
.qodo
|
|
||||||
*.log
|
|
||||||
*.out
|
|
||||||
test_temp
|
|
||||||
|
|||||||
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Ereshor Workspace",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${workspaceFolder}",
|
|
||||||
"cwd": "C:\\Users\\Administrator\\Seafile\\Games-Ereshor"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
89
README.md
89
README.md
@@ -1,89 +0,0 @@
|
|||||||
# 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
|
|
||||||
2
build.sh
2
build.sh
@@ -1,2 +0,0 @@
|
|||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o cln.exe .
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o cln .
|
|
||||||
100
colors.go
100
colors.go
@@ -1,100 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Reset
|
|
||||||
Reset = "\033[0m" // Text Reset
|
|
||||||
|
|
||||||
// Regular Colors
|
|
||||||
Black = "\033[0;30m" // Black
|
|
||||||
Red = "\033[0;31m" // Red
|
|
||||||
Green = "\033[0;32m" // Green
|
|
||||||
Yellow = "\033[0;33m" // Yellow
|
|
||||||
Blue = "\033[0;34m" // Blue
|
|
||||||
Purple = "\033[0;35m" // Purple
|
|
||||||
Cyan = "\033[0;36m" // Cyan
|
|
||||||
White = "\033[0;37m" // White
|
|
||||||
|
|
||||||
// Bold
|
|
||||||
BBlack = "\033[1;30m" // Black
|
|
||||||
BRed = "\033[1;31m" // Red
|
|
||||||
BGreen = "\033[1;32m" // Green
|
|
||||||
BYellow = "\033[1;33m" // Yellow
|
|
||||||
BBlue = "\033[1;34m" // Blue
|
|
||||||
BPurple = "\033[1;35m" // Purple
|
|
||||||
BCyan = "\033[1;36m" // Cyan
|
|
||||||
BWhite = "\033[1;37m" // White
|
|
||||||
|
|
||||||
// Underline
|
|
||||||
UBlack = "\033[4;30m" // Black
|
|
||||||
URed = "\033[4;31m" // Red
|
|
||||||
UGreen = "\033[4;32m" // Green
|
|
||||||
UYellow = "\033[4;33m" // Yellow
|
|
||||||
UBlue = "\033[4;34m" // Blue
|
|
||||||
UPurple = "\033[4;35m" // Purple
|
|
||||||
UCyan = "\033[4;36m" // Cyan
|
|
||||||
UWhite = "\033[4;37m" // White
|
|
||||||
|
|
||||||
// Background
|
|
||||||
On_Black = "\033[40m" // Black
|
|
||||||
On_Red = "\033[41m" // Red
|
|
||||||
On_Green = "\033[42m" // Green
|
|
||||||
On_Yellow = "\033[43m" // Yellow
|
|
||||||
On_Blue = "\033[44m" // Blue
|
|
||||||
On_Purple = "\033[45m" // Purple
|
|
||||||
On_Cyan = "\033[46m" // Cyan
|
|
||||||
On_White = "\033[47m" // White
|
|
||||||
|
|
||||||
// High Intensty
|
|
||||||
IBlack = "\033[0;90m" // Black
|
|
||||||
IRed = "\033[0;91m" // Red
|
|
||||||
IGreen = "\033[0;92m" // Green
|
|
||||||
IYellow = "\033[0;93m" // Yellow
|
|
||||||
IBlue = "\033[0;94m" // Blue
|
|
||||||
IPurple = "\033[0;95m" // Purple
|
|
||||||
ICyan = "\033[0;96m" // Cyan
|
|
||||||
IWhite = "\033[0;97m" // White
|
|
||||||
|
|
||||||
// Bold High Intensty
|
|
||||||
BIBlack = "\033[1;90m" // Black
|
|
||||||
BIRed = "\033[1;91m" // Red
|
|
||||||
BIGreen = "\033[1;92m" // Green
|
|
||||||
BIYellow = "\033[1;93m" // Yellow
|
|
||||||
BIBlue = "\033[1;94m" // Blue
|
|
||||||
BIPurple = "\033[1;95m" // Purple
|
|
||||||
BICyan = "\033[1;96m" // Cyan
|
|
||||||
BIWhite = "\033[1;97m" // White
|
|
||||||
|
|
||||||
// High Intensty backgrounds
|
|
||||||
On_IBlack = "\033[0;100m" // Black
|
|
||||||
On_IRed = "\033[0;101m" // Red
|
|
||||||
On_IGreen = "\033[0;102m" // Green
|
|
||||||
On_IYellow = "\033[0;103m" // Yellow
|
|
||||||
On_IBlue = "\033[0;104m" // Blue
|
|
||||||
On_IPurple = "\033[10;95m" // Purple
|
|
||||||
On_ICyan = "\033[0;106m" // Cyan
|
|
||||||
On_IWhite = "\033[0;107m" // White
|
|
||||||
)
|
|
||||||
|
|
||||||
// The acceptable range is [16, 231] but here we remove some very dark colors
|
|
||||||
// That make text unreadable on a dark terminal
|
|
||||||
// See https://www.hackitu.de/termcolor256/
|
|
||||||
// Wait - why are we hardcoding this? lol do for loops not exist in our universe?
|
|
||||||
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,2 +1 @@
|
|||||||
GOOS=windows GOARCH=amd64 go build -o main.exe main && cp main.exe "/c/Program Files/Git/usr/bin/cln.exe"
|
go build main && cp main.exe "/c/Program Files/Git/usr/bin/cln.exe"
|
||||||
GOOS=linux GOARCH=amd64 go build -o main_linux main
|
|
||||||
180
filesystem.go
180
filesystem.go
@@ -1,180 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var filesystem FileSystem = NewRealFileSystem()
|
|
||||||
|
|
||||||
// FileSystem abstracts filesystem operations so we can swap implementations
|
|
||||||
type FileSystem interface {
|
|
||||||
Remove(path string) error
|
|
||||||
RemoveAll(path string) error
|
|
||||||
MkdirAll(path string, perm os.FileMode) error
|
|
||||||
Symlink(source, target string) error
|
|
||||||
Link(source, target string) error
|
|
||||||
SummaryLines() []string
|
|
||||||
IsDryRun() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
opRemove = "remove"
|
|
||||||
opRemoveAll = "remove_all"
|
|
||||||
opMkdirAll = "mkdir_all"
|
|
||||||
opSymlink = "symlink"
|
|
||||||
opHardlink = "hardlink"
|
|
||||||
)
|
|
||||||
|
|
||||||
type operationRecord struct {
|
|
||||||
kind string
|
|
||||||
source string
|
|
||||||
target string
|
|
||||||
err error
|
|
||||||
dryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r operationRecord) summaryLine() (string, bool) {
|
|
||||||
if r.kind != opSymlink && r.kind != opHardlink {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
kindLabel := "Symlink"
|
|
||||||
if r.kind == opHardlink {
|
|
||||||
kindLabel = "Hardlink"
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "OK"
|
|
||||||
if r.err != nil {
|
|
||||||
status = fmt.Sprintf("FAIL: %v", r.err)
|
|
||||||
} else if r.dryRun {
|
|
||||||
status = "DRY-RUN"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s %s -> %s (%s)", kindLabel, r.source, r.target, status), true
|
|
||||||
}
|
|
||||||
|
|
||||||
type baseFileSystem struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
operations []operationRecord
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *baseFileSystem) addOperation(kind, source, target string, err error, dryRun bool) {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
b.operations = append(b.operations, operationRecord{
|
|
||||||
kind: kind,
|
|
||||||
source: source,
|
|
||||||
target: target,
|
|
||||||
err: err,
|
|
||||||
dryRun: dryRun,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *baseFileSystem) snapshot() []operationRecord {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
ops := make([]operationRecord, len(b.operations))
|
|
||||||
copy(ops, b.operations)
|
|
||||||
return ops
|
|
||||||
}
|
|
||||||
|
|
||||||
func summarizeOperations(records []operationRecord) []string {
|
|
||||||
var lines []string
|
|
||||||
for _, record := range records {
|
|
||||||
if line, ok := record.summaryLine(); ok {
|
|
||||||
lines = append(lines, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
type realFileSystem struct {
|
|
||||||
baseFileSystem
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRealFileSystem returns a filesystem implementation that writes to disk
|
|
||||||
func NewRealFileSystem() FileSystem {
|
|
||||||
return &realFileSystem{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *realFileSystem) Remove(path string) error {
|
|
||||||
err := os.Remove(path)
|
|
||||||
fs.addOperation(opRemove, "", path, err, false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *realFileSystem) RemoveAll(path string) error {
|
|
||||||
err := os.RemoveAll(path)
|
|
||||||
fs.addOperation(opRemoveAll, "", path, err, false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *realFileSystem) MkdirAll(path string, perm os.FileMode) error {
|
|
||||||
err := os.MkdirAll(path, perm)
|
|
||||||
fs.addOperation(opMkdirAll, "", path, err, false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *realFileSystem) Symlink(source, target string) error {
|
|
||||||
err := os.Symlink(source, target)
|
|
||||||
fs.addOperation(opSymlink, source, target, err, false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *realFileSystem) Link(source, target string) error {
|
|
||||||
err := os.Link(source, target)
|
|
||||||
fs.addOperation(opHardlink, source, target, err, false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *realFileSystem) SummaryLines() []string {
|
|
||||||
return summarizeOperations(fs.snapshot())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *realFileSystem) IsDryRun() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type dryRunFileSystem struct {
|
|
||||||
baseFileSystem
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDryRunFileSystem returns a filesystem implementation that only records operations
|
|
||||||
func NewDryRunFileSystem() FileSystem {
|
|
||||||
return &dryRunFileSystem{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *dryRunFileSystem) Remove(path string) error {
|
|
||||||
fs.addOperation(opRemove, "", path, nil, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *dryRunFileSystem) RemoveAll(path string) error {
|
|
||||||
fs.addOperation(opRemoveAll, "", path, nil, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *dryRunFileSystem) MkdirAll(path string, perm os.FileMode) error {
|
|
||||||
fs.addOperation(opMkdirAll, "", path, nil, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *dryRunFileSystem) Symlink(source, target string) error {
|
|
||||||
fs.addOperation(opSymlink, source, target, nil, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *dryRunFileSystem) Link(source, target string) error {
|
|
||||||
fs.addOperation(opHardlink, source, target, nil, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *dryRunFileSystem) SummaryLines() []string {
|
|
||||||
return summarizeOperations(fs.snapshot())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *dryRunFileSystem) IsDryRun() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
18
go.mod
18
go.mod
@@ -1,17 +1,3 @@
|
|||||||
module cln
|
module main
|
||||||
|
|
||||||
go 1.23.6
|
go 1.21.7
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.site.quack-lab.dev/dave/cyutils v1.4.0
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
golang.org/x/time v0.12.0 // indirect
|
|
||||||
)
|
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,16 +0,0 @@
|
|||||||
git.site.quack-lab.dev/dave/cyutils v1.4.0 h1:/Xo3QfLIFNab+axHneWmUK4MyfuObl+qq8whF9vTQpk=
|
|
||||||
git.site.quack-lab.dev/dave/cyutils v1.4.0/go.mod h1:fBjALu2Cp2u2bDr+E4zbGVMBeIgFzROg+4TCcTNAiQU=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
|
||||||
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=
|
|
||||||
49
home_test.go
49
home_test.go
@@ -1,49 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHomeDirectoryPatternExpansion(t *testing.T) {
|
|
||||||
testDir := getTestSubDir(t)
|
|
||||||
|
|
||||||
// Ensure we're working within the project directory
|
|
||||||
ensureInProjectDir(t, testDir)
|
|
||||||
|
|
||||||
// Change to test directory
|
|
||||||
originalDir, _ := os.Getwd()
|
|
||||||
defer os.Chdir(originalDir)
|
|
||||||
os.Chdir(testDir)
|
|
||||||
|
|
||||||
// Get the actual home directory
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Create a test directory in the home folder
|
|
||||||
testHomeDir := filepath.Join(homeDir, "synclib_test")
|
|
||||||
err = os.MkdirAll(testHomeDir, 0755)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer os.RemoveAll(testHomeDir) // Cleanup
|
|
||||||
|
|
||||||
// Create a test file in the home directory
|
|
||||||
testFile := filepath.Join(testHomeDir, "testhome.csv")
|
|
||||||
err = os.WriteFile(testFile, []byte("test content"), 0644)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Test the pattern with ~/ that should match the file
|
|
||||||
pattern := "~/synclib_test/testhome.csv"
|
|
||||||
links, err := ExpandPattern(pattern, testDir, "target.csv", false)
|
|
||||||
|
|
||||||
// This should work but currently fails due to the bug
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, len(links), "Pattern should match exactly 1 file")
|
|
||||||
|
|
||||||
if len(links) > 0 {
|
|
||||||
assert.Contains(t, links[0].Source, "testhome.csv")
|
|
||||||
assert.Equal(t, "target.csv", links[0].Target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
735
instruction.go
735
instruction.go
@@ -1,603 +1,132 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"fmt"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"regexp"
|
||||||
"strings"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
)
|
||||||
"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))
|
||||||
Files bool `yaml:"files,omitempty"`
|
}
|
||||||
}
|
|
||||||
|
func ParseInstruction(line string) (LinkInstruction, error) {
|
||||||
type YAMLConfig struct {
|
parts := strings.Split(line, deliminer)
|
||||||
Links []LinkInstruction `yaml:"links"`
|
instruction := LinkInstruction{}
|
||||||
From []string `yaml:"from,omitempty"`
|
|
||||||
}
|
if len(parts) < 2 {
|
||||||
|
return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)")
|
||||||
func (instruction *LinkInstruction) Tidy() {
|
}
|
||||||
instruction.Source = strings.ReplaceAll(instruction.Source, "\"", "")
|
|
||||||
instruction.Source = strings.ReplaceAll(instruction.Source, "\\", "/")
|
instruction.Source = parts[0]
|
||||||
instruction.Source = strings.TrimSpace(instruction.Source)
|
instruction.Target = parts[1]
|
||||||
|
instruction.Force = false
|
||||||
instruction.Target = strings.ReplaceAll(instruction.Target, "\"", "")
|
if len(parts) > 2 {
|
||||||
instruction.Target = strings.ReplaceAll(instruction.Target, "\\", "/")
|
res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2])
|
||||||
instruction.Target = strings.TrimSpace(instruction.Target)
|
instruction.Force = res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instruction *LinkInstruction) String() string {
|
instruction.Source, _ = ConvertHome(instruction.Source)
|
||||||
var flags []string
|
instruction.Target, _ = ConvertHome(instruction.Target)
|
||||||
if instruction.Force {
|
|
||||||
flags = append(flags, "force=true")
|
instruction.Source = NormalizePath(instruction.Source)
|
||||||
}
|
instruction.Target = NormalizePath(instruction.Target)
|
||||||
if instruction.Hard {
|
|
||||||
flags = append(flags, "hard=true")
|
return instruction, nil
|
||||||
}
|
}
|
||||||
if instruction.Delete {
|
|
||||||
flags = append(flags, "delete=true")
|
func (instruction *LinkInstruction) RunSync() error {
|
||||||
}
|
if !FileExists(instruction.Source) {
|
||||||
if instruction.Files {
|
return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
|
||||||
flags = append(flags, "files=true")
|
}
|
||||||
}
|
|
||||||
|
if AreSame(instruction.Source, instruction.Target) {
|
||||||
flagsStr := ""
|
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)
|
||||||
if len(flags) > 0 {
|
return nil
|
||||||
flagsStr = " [" + strings.Join(flags, ", ") + "]"
|
}
|
||||||
}
|
|
||||||
|
if FileExists(instruction.Target) {
|
||||||
return fmt.Sprintf("%s → %s%s",
|
if instruction.Force {
|
||||||
FormatSourcePath(instruction.Source),
|
isSymlink, err := IsSymlink(instruction.Target)
|
||||||
FormatTargetPath(instruction.Target),
|
if err != nil {
|
||||||
flagsStr)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instruction *LinkInstruction) Undo() {
|
if isSymlink {
|
||||||
if !FileExists(instruction.Target) {
|
log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
||||||
LogInfo("%s does not exist, skipping", FormatTargetPath(instruction.Target))
|
err = os.Remove(instruction.Target)
|
||||||
return
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
|
}
|
||||||
isSymlink, err := IsSymlink(instruction.Target)
|
} else {
|
||||||
if err != nil {
|
return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
||||||
LogError("could not determine whether %s is a sym link or not, stopping; err: %v",
|
}
|
||||||
FormatTargetPath(instruction.Target), err)
|
} else {
|
||||||
return
|
return fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if isSymlink {
|
|
||||||
LogInfo("Removing symlink at %s", FormatTargetPath(instruction.Target))
|
err := os.Symlink(instruction.Source, instruction.Target)
|
||||||
err = filesystem.Remove(instruction.Target)
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed creating symlink between %s%s%s and %s%s%s with error %s%+v%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
LogError("could not remove symlink at %s; err: %v",
|
}
|
||||||
FormatTargetPath(instruction.Target), err)
|
log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
|
||||||
} else {
|
|
||||||
if filesystem.IsDryRun() {
|
return nil
|
||||||
LogInfo("[DRY-RUN] Would remove symlink at %s", FormatTargetPath(instruction.Target))
|
}
|
||||||
} else {
|
|
||||||
LogSuccess("Removed symlink at %s", FormatTargetPath(instruction.Target))
|
func (instruction *LinkInstruction) RunAsync(status chan (error)) {
|
||||||
}
|
defer close(status)
|
||||||
}
|
if !FileExists(instruction.Source) {
|
||||||
} else {
|
status <- fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
|
||||||
LogInfo("%s is not a symlink, skipping", FormatTargetPath(instruction.Target))
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if AreSame(instruction.Source, instruction.Target) {
|
||||||
func ParseInstruction(line, workdir string) (LinkInstruction, error) {
|
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)
|
||||||
line = strings.TrimSpace(line)
|
return
|
||||||
if strings.HasPrefix(line, "#") {
|
}
|
||||||
return LinkInstruction{}, fmt.Errorf("comment line")
|
|
||||||
}
|
if FileExists(instruction.Target) {
|
||||||
|
if instruction.Force {
|
||||||
parts := strings.Split(line, deliminer)
|
isSymlink, err := IsSymlink(instruction.Target)
|
||||||
instruction := LinkInstruction{}
|
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)
|
||||||
if len(parts) < 2 {
|
return
|
||||||
return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)")
|
}
|
||||||
}
|
|
||||||
|
if isSymlink {
|
||||||
instruction.Source = strings.TrimSpace(parts[0])
|
log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
||||||
instruction.Target = strings.TrimSpace(parts[1])
|
err = os.Remove(instruction.Target)
|
||||||
|
if err != nil {
|
||||||
for i := 2; i < len(parts); i++ {
|
status <- fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
flagPart := strings.TrimSpace(parts[i])
|
return
|
||||||
|
}
|
||||||
// Support for legacy format (backward compatibility)
|
} else {
|
||||||
if !strings.Contains(flagPart, "=") {
|
status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
|
||||||
// Legacy format: positional boolean flags
|
return
|
||||||
switch i {
|
}
|
||||||
case 2: // Force flag (3rd position)
|
} else {
|
||||||
instruction.Force = isTrue(flagPart)
|
status <- fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor)
|
||||||
case 3: // Hard flag (4th position)
|
return
|
||||||
instruction.Hard = isTrue(flagPart)
|
}
|
||||||
case 4: // Delete flag (5th position)
|
}
|
||||||
instruction.Delete = isTrue(flagPart)
|
|
||||||
if instruction.Delete {
|
err := os.Symlink(instruction.Source, instruction.Target)
|
||||||
instruction.Force = true // Delete implies Force
|
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)
|
||||||
case 5: // Files flag (6th position)
|
return
|
||||||
instruction.Files = isTrue(flagPart)
|
}
|
||||||
}
|
log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
|
||||||
continue
|
|
||||||
}
|
status <- nil
|
||||||
|
}
|
||||||
// New format: named flags (name=value)
|
|
||||||
nameValue := strings.SplitN(flagPart, "=", 2)
|
|
||||||
if len(nameValue) != 2 {
|
|
||||||
// Skip malformed flags
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
flagName := strings.ToLower(strings.TrimSpace(nameValue[0]))
|
|
||||||
flagValue := strings.TrimSpace(nameValue[1])
|
|
||||||
|
|
||||||
switch flagName {
|
|
||||||
case "force", "f":
|
|
||||||
instruction.Force = isTrue(flagValue)
|
|
||||||
case "hard", "h":
|
|
||||||
instruction.Hard = isTrue(flagValue)
|
|
||||||
case "delete", "d":
|
|
||||||
instruction.Delete = isTrue(flagValue)
|
|
||||||
if instruction.Delete {
|
|
||||||
instruction.Force = true // Delete implies Force
|
|
||||||
}
|
|
||||||
case "files":
|
|
||||||
instruction.Files = isTrue(flagValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instruction.Tidy()
|
|
||||||
instruction.Source, _ = ConvertHome(instruction.Source)
|
|
||||||
instruction.Target, _ = ConvertHome(instruction.Target)
|
|
||||||
|
|
||||||
instruction.Source = NormalizePath(instruction.Source, workdir)
|
|
||||||
instruction.Target = NormalizePath(instruction.Target, workdir)
|
|
||||||
|
|
||||||
return instruction, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTrue(value string) bool {
|
|
||||||
value = strings.ToLower(strings.TrimSpace(value))
|
|
||||||
return value == "true" || value == "t" || value == "yes" || value == "y" || value == "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (instruction *LinkInstruction) RunAsync(status chan (error)) {
|
|
||||||
defer close(status)
|
|
||||||
if undo {
|
|
||||||
instruction.Undo()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !FileExists(instruction.Source) {
|
|
||||||
status <- fmt.Errorf("instruction source %s does not exist", FormatSourcePath(instruction.Source))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if instruction.Files {
|
|
||||||
info, err := os.Stat(instruction.Source)
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("could not stat source %s; err: %v", FormatSourcePath(instruction.Source), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
status <- fmt.Errorf("source %s is a directory but files=true", FormatSourcePath(instruction.Source))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !instruction.Force && AreSame(instruction.Source, instruction.Target) {
|
|
||||||
//status <- fmt.Errorf("source %s and target %s are the same, nothing to do...",
|
|
||||||
// FormatSourcePath(instruction.Source),
|
|
||||||
// FormatTargetPath(instruction.Target))
|
|
||||||
LogInfo("Source %s and target %s are the same, nothing to do...",
|
|
||||||
FormatSourcePath(instruction.Source),
|
|
||||||
FormatTargetPath(instruction.Target))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if FileExists(instruction.Target) {
|
|
||||||
if instruction.Files {
|
|
||||||
if info, err := os.Stat(instruction.Target); err == nil && info.IsDir() {
|
|
||||||
status <- fmt.Errorf("target %s is a directory but files=true", FormatTargetPath(instruction.Target))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if instruction.Force {
|
|
||||||
isSymlink, err := IsSymlink(instruction.Target)
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("could not determine whether %s is a sym link or not, stopping; err: %v",
|
|
||||||
FormatTargetPath(instruction.Target), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if instruction.Hard {
|
|
||||||
info, err := os.Stat(instruction.Target)
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("could not stat %s, stopping; err: %v",
|
|
||||||
FormatTargetPath(instruction.Target), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if info.Mode().IsRegular() && info.Name() == filepath.Base(instruction.Source) {
|
|
||||||
LogTarget("Overwriting existing file %s", instruction.Target)
|
|
||||||
err := filesystem.Remove(instruction.Target)
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("could not remove existing file %s; err: %v",
|
|
||||||
FormatTargetPath(instruction.Target), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSymlink {
|
|
||||||
LogTarget("Removing symlink at %s", instruction.Target)
|
|
||||||
err = filesystem.Remove(instruction.Target)
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("failed deleting %s due to %v",
|
|
||||||
FormatTargetPath(instruction.Target), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !instruction.Delete {
|
|
||||||
status <- fmt.Errorf("refusing to delte actual (non symlink) file %s",
|
|
||||||
FormatTargetPath(instruction.Target))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
LogImportant("Deleting (!!!) %s", instruction.Target)
|
|
||||||
err = filesystem.RemoveAll(instruction.Target)
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("failed deleting %s due to %v",
|
|
||||||
FormatTargetPath(instruction.Target), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
status <- fmt.Errorf("target %s exists - handle manually or set the 'forced' flag (3rd field)",
|
|
||||||
FormatTargetPath(instruction.Target))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
targetDir := filepath.Dir(instruction.Target)
|
|
||||||
if _, err := os.Stat(targetDir); os.IsNotExist(err) {
|
|
||||||
err = filesystem.MkdirAll(targetDir, 0755)
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("failed creating directory %s due to %v",
|
|
||||||
FormatTargetPath(targetDir), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
linkType := "symlink"
|
|
||||||
if instruction.Hard {
|
|
||||||
linkType = "hardlink"
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if instruction.Hard {
|
|
||||||
err = filesystem.Link(instruction.Source, instruction.Target)
|
|
||||||
} else {
|
|
||||||
err = filesystem.Symlink(instruction.Source, instruction.Target)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
status <- fmt.Errorf("failed creating %s between %s and %s with error %v",
|
|
||||||
linkType,
|
|
||||||
FormatSourcePath(instruction.Source),
|
|
||||||
FormatTargetPath(instruction.Target),
|
|
||||||
err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if filesystem.IsDryRun() {
|
|
||||||
LogInfo("[DRY-RUN] Would create %s between %s and %s",
|
|
||||||
linkType,
|
|
||||||
FormatSourcePath(instruction.Source),
|
|
||||||
FormatTargetPath(instruction.Target))
|
|
||||||
} else {
|
|
||||||
LogSuccess("Created %s between %s and %s",
|
|
||||||
linkType,
|
|
||||||
FormatSourcePath(instruction.Source),
|
|
||||||
FormatTargetPath(instruction.Target))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preprocess instructions: expand globs and from references
|
|
||||||
// Create a new visited map for this file
|
|
||||||
visited := make(map[string]bool)
|
|
||||||
processedInstructions, err := preprocessInstructions(instructions, filename, workdir, visited)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final processing: normalize paths and set defaults
|
|
||||||
for i := range processedInstructions {
|
|
||||||
link := &processedInstructions[i]
|
|
||||||
link.Tidy()
|
|
||||||
link.Source, _ = ConvertHome(link.Source)
|
|
||||||
link.Target, _ = ConvertHome(link.Target)
|
|
||||||
link.Source = NormalizePath(link.Source, workdir)
|
|
||||||
link.Target = NormalizePath(link.Target, workdir)
|
|
||||||
|
|
||||||
// If Delete is true, Force must also be true
|
|
||||||
if link.Delete {
|
|
||||||
link.Force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedInstructions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// preprocessInstructions handles glob expansion and from references
|
|
||||||
func preprocessInstructions(instructions []LinkInstruction, filename, workdir string, visited map[string]bool) ([]LinkInstruction, error) {
|
|
||||||
var result []LinkInstruction
|
|
||||||
|
|
||||||
for _, instr := range instructions {
|
|
||||||
if instr.Source == "" {
|
|
||||||
continue // Skip invalid instructions
|
|
||||||
}
|
|
||||||
|
|
||||||
if instr.Target == "" {
|
|
||||||
// This is a from reference - load the referenced file
|
|
||||||
fromInstructions, err := loadFromReference(instr.Source, filename, workdir, visited)
|
|
||||||
if err != nil {
|
|
||||||
var absRefErr *absoluteReferenceError
|
|
||||||
if errors.As(err, &absRefErr) {
|
|
||||||
LogError("Referenced file not found: %s (from %s), skipping", instr.Source, filename)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
LogError("Referenced file not found: %s (from %s), stopping", instr.Source, filename)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("error loading from reference %s: %w", instr.Source, err)
|
|
||||||
}
|
|
||||||
result = append(result, fromInstructions...)
|
|
||||||
} else {
|
|
||||||
// This is a regular instruction - expand globs if needed
|
|
||||||
expandedInstructions, err := expandGlobs(instr, filename, workdir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error expanding globs for %s: %w", instr.Source, err)
|
|
||||||
}
|
|
||||||
result = append(result, expandedInstructions...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadFromReference loads instructions from a referenced file
|
|
||||||
type absoluteReferenceError struct {
|
|
||||||
path string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *absoluteReferenceError) Error() string {
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *absoluteReferenceError) Unwrap() error {
|
|
||||||
return e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadFromReference(fromFile, currentFile, workdir string, visited map[string]bool) ([]LinkInstruction, error) {
|
|
||||||
// First convert home directory if it starts with ~
|
|
||||||
fromPath, err := ConvertHome(fromFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting home directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
refIsAbsolute := filepath.IsAbs(fromPath)
|
|
||||||
|
|
||||||
// Convert relative paths to absolute paths based on the current file's directory
|
|
||||||
if !filepath.IsAbs(fromPath) {
|
|
||||||
currentDir := filepath.Dir(currentFile)
|
|
||||||
fromPath = filepath.Join(currentDir, fromPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize the path
|
|
||||||
fromPath = filepath.Clean(fromPath)
|
|
||||||
|
|
||||||
// Recursively parse the referenced file with cycle detection
|
|
||||||
fromWorkdir := filepath.Dir(fromPath)
|
|
||||||
links, err := parseYAMLFileRecursive(fromPath, fromWorkdir, visited)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) && refIsAbsolute {
|
|
||||||
return nil, &absoluteReferenceError{path: fromPath, err: err}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return links, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// expandGlobs expands glob patterns in a single instruction
|
|
||||||
func expandGlobs(instr LinkInstruction, filename, workdir string) ([]LinkInstruction, error) {
|
|
||||||
// Convert home directory (~) before expanding pattern
|
|
||||||
convertedSource, err := ConvertHome(instr.Source)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting home directory in source %s: %w", instr.Source, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
LogSource("Expanding pattern source %s in YAML file %s", convertedSource, filename)
|
|
||||||
newlinks, err := ExpandPattern(convertedSource, workdir, instr.Target, instr.Files)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone the original instruction properties for each expanded link
|
|
||||||
for i := range newlinks {
|
|
||||||
newlinks[i].Delete = instr.Delete
|
|
||||||
newlinks[i].Hard = instr.Hard
|
|
||||||
newlinks[i].Force = instr.Force
|
|
||||||
newlinks[i].Files = instr.Files
|
|
||||||
}
|
|
||||||
|
|
||||||
LogInfo("Expanded pattern %s in YAML file %s to %d links",
|
|
||||||
FormatSourcePath(instr.Source), FormatSourcePath(filename), len(newlinks))
|
|
||||||
|
|
||||||
return newlinks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseYAMLFileRecursive parses a YAML file and recursively processes any "From" references
|
|
||||||
func ParseYAMLFileRecursive(filename, workdir string) ([]LinkInstruction, error) {
|
|
||||||
visited := make(map[string]bool)
|
|
||||||
return parseYAMLFileRecursive(filename, workdir, visited)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseYAMLFileRecursive is the internal recursive function that tracks visited files to prevent cycles
|
|
||||||
func parseYAMLFileRecursive(filename, workdir string, visited map[string]bool) ([]LinkInstruction, error) {
|
|
||||||
// Normalize the filename to prevent cycles with different path representations
|
|
||||||
normalizedFilename, err := filepath.Abs(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error normalizing filename: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for cycles
|
|
||||||
if visited[normalizedFilename] {
|
|
||||||
return nil, fmt.Errorf("circular reference detected: %s", filename)
|
|
||||||
}
|
|
||||||
visited[normalizedFilename] = true
|
|
||||||
defer delete(visited, normalizedFilename)
|
|
||||||
|
|
||||||
// Parse the current file and preprocess it with cycle detection
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading YAML file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preprocess instructions: expand globs and from references
|
|
||||||
processedInstructions, err := preprocessInstructions(instructions, filename, workdir, visited)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final processing: normalize paths and set defaults
|
|
||||||
for i := range processedInstructions {
|
|
||||||
link := &processedInstructions[i]
|
|
||||||
link.Tidy()
|
|
||||||
link.Source, _ = ConvertHome(link.Source)
|
|
||||||
link.Target, _ = ConvertHome(link.Target)
|
|
||||||
link.Source = NormalizePath(link.Source, workdir)
|
|
||||||
link.Target = NormalizePath(link.Target, workdir)
|
|
||||||
|
|
||||||
// If Delete is true, Force must also be true
|
|
||||||
if link.Delete {
|
|
||||||
link.Force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedInstructions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExpandPattern(source, workdir, target string, filesOnly bool) (links []LinkInstruction, err error) {
|
|
||||||
if strings.TrimSpace(source) == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
// Convert home directory (~) before splitting pattern
|
|
||||||
source, err = ConvertHome(source)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting home directory in source %s: %w", source, err)
|
|
||||||
}
|
|
||||||
// Normalize path to convert backslashes to forward slashes before pattern processing
|
|
||||||
source = NormalizePath(source, workdir)
|
|
||||||
|
|
||||||
if !strings.ContainsAny(source, "*?[{") {
|
|
||||||
info, statErr := os.Stat(source)
|
|
||||||
if statErr != nil {
|
|
||||||
if os.IsNotExist(statErr) {
|
|
||||||
LogInfo("Literal source %s does not exist, skipping", FormatSourcePath(source))
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to stat literal source %s: %w", source, statErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filesOnly && info.IsDir() {
|
|
||||||
LogInfo("Files-only mode: skipping directory %s", FormatSourcePath(source))
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return []LinkInstruction{
|
|
||||||
{
|
|
||||||
Source: source,
|
|
||||||
Target: target,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
static, pattern := doublestar.SplitPattern(source)
|
|
||||||
|
|
||||||
if static == "" || static == "." {
|
|
||||||
static = workdir
|
|
||||||
}
|
|
||||||
LogInfo("Static part: %s", static)
|
|
||||||
LogInfo("Pattern part: %s", pattern)
|
|
||||||
|
|
||||||
files, err := doublestar.Glob(os.DirFS(static), pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error expanding pattern: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
fullPath := filepath.Join(static, file)
|
|
||||||
|
|
||||||
info, err := os.Stat(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Failed to stat %s: %v", FormatSourcePath(fullPath), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if filesOnly && info.IsDir() {
|
|
||||||
LogInfo("Files-only mode: skipping directory %s", FormatSourcePath(fullPath))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
// We don't care about matched directories
|
|
||||||
// We want files within them
|
|
||||||
LogInfo("Skipping directory %s", file)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath := filepath.Join(target, file)
|
|
||||||
|
|
||||||
links = append(links, LinkInstruction{
|
|
||||||
Source: fullPath,
|
|
||||||
Target: targetPath,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
LogInfo("Expanded pattern %s to %d links", FormatSourcePath(source), len(links))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsYAMLFile(filename string) bool {
|
|
||||||
ext := strings.ToLower(filepath.Ext(filename))
|
|
||||||
return ext == ".yaml" || ext == ".yml"
|
|
||||||
}
|
|
||||||
4561
instruction_test.go
4561
instruction_test.go
File diff suppressed because it is too large
Load Diff
99
logger.go
99
logger.go
@@ -1,99 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message type prefixes
|
|
||||||
const (
|
|
||||||
InfoPrefix = "INFO"
|
|
||||||
ErrorPrefix = "ERROR"
|
|
||||||
WarningPrefix = "WARN"
|
|
||||||
SourcePrefix = "SOURCE"
|
|
||||||
TargetPrefix = "TARGET"
|
|
||||||
PathPrefix = "PATH"
|
|
||||||
ImportantPrefix = "IMPORTANT"
|
|
||||||
SuccessPrefix = "DONE"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LogInfo logs an informational message
|
|
||||||
func LogInfo(format string, args ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
log.Printf("%s[%s]%s %s", BGreen, InfoPrefix, Reset, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogSuccess logs a success message
|
|
||||||
func LogSuccess(format string, args ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
log.Printf("%s%s[%s]%s %s", BBlue, On_Blue, SuccessPrefix, Reset, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogSource logs a message about a source file/path with proper coloring
|
|
||||||
func LogSource(format string, args ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
log.Printf("%s[%s]%s %s%s%s", BPurple, SourcePrefix, Reset, SourceColor, message, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogTarget logs a message about a target file/path with proper coloring
|
|
||||||
func LogTarget(format string, args ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
log.Printf("%s[%s]%s %s%s%s", BYellow, TargetPrefix, Reset, TargetColor, message, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogPath logs a message about a path with proper coloring
|
|
||||||
func LogPath(format string, args ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
log.Printf("%s[%s]%s %s%s%s", BGreen, PathPrefix, Reset, PathColor, message, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogImportant logs a message that needs attention with proper coloring
|
|
||||||
func LogImportant(format string, args ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
log.Printf("%s[%s]%s %s%s%s", BRed, ImportantPrefix, Reset, ImportantColor, message, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogError logs an error message with proper coloring that won't be cut off
|
|
||||||
func LogError(format string, args ...interface{}) {
|
|
||||||
// Format the message first with normal text (no red coloring)
|
|
||||||
message := fmt.Sprintf(format, args...)
|
|
||||||
|
|
||||||
// The Error prefix itself is bold red on a light background for maximum visibility
|
|
||||||
prefix := fmt.Sprintf("%s%s[%s]%s ", BRed, On_White, ErrorPrefix, Reset)
|
|
||||||
|
|
||||||
// The message is in default color (no red), only the [ERROR] prefix is colored
|
|
||||||
log.Printf("%s%s", prefix, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatSourcePath returns a source path with proper coloring
|
|
||||||
func FormatSourcePath(path string) string {
|
|
||||||
return fmt.Sprintf("%s%s%s", SourceColor, path, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatTargetPath returns a target path with proper coloring
|
|
||||||
func FormatTargetPath(path string) string {
|
|
||||||
return fmt.Sprintf("%s%s%s", TargetColor, path, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatPathValue returns a path with proper coloring
|
|
||||||
func FormatPathValue(path string) string {
|
|
||||||
return fmt.Sprintf("%s%s%s", PathColor, path, Reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatErrorValue returns an error value without any additional formatting
|
|
||||||
// Since error messages are no longer red, we don't need special formatting
|
|
||||||
func FormatErrorValue(err error) string {
|
|
||||||
if err == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// Just return the error string with no color formatting
|
|
||||||
return fmt.Sprintf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatErrorMessage formats an error message with no additional color formatting,
|
|
||||||
// while preserving the special formatting of embedded source/target/path elements.
|
|
||||||
func FormatErrorMessage(format string, args ...interface{}) string {
|
|
||||||
// This just formats the message with no additional color formatting
|
|
||||||
// The path formatting will still be preserved
|
|
||||||
return fmt.Sprintf(format, args...)
|
|
||||||
}
|
|
||||||
578
main.go
578
main.go
@@ -1,340 +1,238 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"regexp"
|
||||||
"sync/atomic"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
utils "git.site.quack-lab.dev/dave/cyutils"
|
)
|
||||||
)
|
|
||||||
|
const deliminer = ","
|
||||||
const deliminer = ","
|
const (
|
||||||
const SourceColor = Purple
|
Black = "\033[30m"
|
||||||
const TargetColor = Yellow
|
Red = "\033[31m"
|
||||||
const ErrorColor = Red
|
Green = "\033[32m"
|
||||||
const ImportantColor = BRed
|
Yellow = "\033[33m"
|
||||||
const DefaultColor = Reset
|
Blue = "\033[34m"
|
||||||
const PathColor = Green
|
Magenta = "\033[35m"
|
||||||
|
Cyan = "\033[36m"
|
||||||
var programName = os.Args[0]
|
White = "\033[37m"
|
||||||
var undo = false
|
)
|
||||||
|
const SourceColor = Magenta
|
||||||
func main() {
|
const TargetColor = Yellow
|
||||||
recurse := flag.String("r", "", "recurse into directories")
|
const ErrorColor = Red
|
||||||
file := flag.String("f", "", "file to read instructions from")
|
const DefaultColor = White
|
||||||
debug := flag.Bool("d", false, "debug")
|
const PathColor = Green
|
||||||
undoF := flag.Bool("u", false, "undo")
|
|
||||||
dryRun := flag.Bool("n", false, "dry run (no filesystem changes)")
|
var DirRegex, _ = regexp.Compile(`^(.+?)[/\\]sync$`)
|
||||||
flag.Parse()
|
var FileRegex, _ = regexp.Compile(`^sync$`)
|
||||||
undo = *undoF
|
var programName = os.Args[0]
|
||||||
|
|
||||||
setupLogging(*debug)
|
func main() {
|
||||||
|
recurse := flag.String("r", "", "recurse into directories")
|
||||||
if *dryRun {
|
file := flag.String("f", "", "file to read instructions from")
|
||||||
filesystem = NewDryRunFileSystem()
|
debug := flag.Bool("d", false, "debug")
|
||||||
LogInfo("Dry run mode enabled - no filesystem changes will be made")
|
flag.Parse()
|
||||||
} else {
|
|
||||||
filesystem = NewRealFileSystem()
|
if *debug {
|
||||||
}
|
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
||||||
|
logFile, err := os.Create("main.log")
|
||||||
instructions := make(chan *LinkInstruction, 1000)
|
if err != nil {
|
||||||
status := make(chan error)
|
log.Printf("Error creating log file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
startInputSource(*recurse, *file, instructions, status)
|
}
|
||||||
|
logger := io.MultiWriter(os.Stdout, logFile)
|
||||||
go handleStatusErrors(status)
|
log.SetOutput(logger)
|
||||||
|
} else {
|
||||||
instructionsDone := processInstructions(instructions)
|
log.SetFlags(log.Lmicroseconds)
|
||||||
|
}
|
||||||
if instructionsDone == 0 {
|
|
||||||
LogInfo("No instructions were processed")
|
log.Printf("Recurse: %s", *recurse)
|
||||||
printSummary(filesystem)
|
log.Printf("File: %s", *file)
|
||||||
os.Exit(1)
|
|
||||||
}
|
instructions := make(chan LinkInstruction, 1000)
|
||||||
LogInfo("All done")
|
status := make(chan error)
|
||||||
printSummary(filesystem)
|
if *recurse != "" {
|
||||||
}
|
go ReadFromFilesRecursively(*recurse, instructions, status)
|
||||||
|
} else if *file != "" {
|
||||||
// setupLogging configures logging based on debug flag
|
go ReadFromFile(*file, instructions, status, true)
|
||||||
func setupLogging(debug bool) {
|
} else if len(os.Args) > 1 {
|
||||||
if debug {
|
go ReadFromArgs(instructions, status)
|
||||||
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
} else {
|
||||||
logFile, err := os.Create(programName + ".log")
|
go ReadFromStdin(instructions, status)
|
||||||
if err != nil {
|
}
|
||||||
LogError("Error creating log file: %v", err)
|
|
||||||
os.Exit(1)
|
go func() {
|
||||||
}
|
for {
|
||||||
logger := io.MultiWriter(os.Stdout, logFile)
|
err, ok := <-status
|
||||||
log.SetOutput(logger)
|
if !ok {
|
||||||
} else {
|
break
|
||||||
log.SetFlags(log.Lmicroseconds)
|
}
|
||||||
}
|
if err != nil {
|
||||||
}
|
log.Println(err)
|
||||||
|
}
|
||||||
// startInputSource determines and starts the appropriate input source
|
}
|
||||||
func startInputSource(recurse, file string, instructions chan *LinkInstruction, status chan error) {
|
}()
|
||||||
// Check input sources in priority order
|
|
||||||
switch {
|
var instructionsDone int32
|
||||||
case recurse != "":
|
var wg sync.WaitGroup
|
||||||
LogInfo("Recurse: %s", recurse)
|
for {
|
||||||
go ReadFromFilesRecursively(recurse, instructions, status)
|
instruction, ok := <-instructions
|
||||||
|
if !ok {
|
||||||
case file != "":
|
log.Printf("No more instructions to process")
|
||||||
LogInfo("File: %s", file)
|
break
|
||||||
go ReadFromFile(file, instructions, status, true)
|
}
|
||||||
|
log.Printf("Processing: %s", instruction.String())
|
||||||
case len(flag.Args()) > 0:
|
status := make(chan error)
|
||||||
LogInfo("Reading from command line arguments")
|
go instruction.RunAsync(status)
|
||||||
go ReadFromArgs(instructions, status)
|
wg.Add(1)
|
||||||
|
err := <-status
|
||||||
// case IsPipeInput():
|
if err != nil {
|
||||||
// LogInfo("Reading from stdin pipe")
|
log.Printf("Failed parsing instruction %s%s%s due to %s%+v%s", SourceColor, instruction.String(), DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
// go ReadFromStdin(instructions, status)
|
}
|
||||||
|
atomic.AddInt32(&instructionsDone, 1)
|
||||||
default:
|
wg.Done()
|
||||||
startDefaultInputSource(instructions, status)
|
}
|
||||||
}
|
wg.Wait()
|
||||||
}
|
log.Println("All done")
|
||||||
|
if instructionsDone == 0 {
|
||||||
// startDefaultInputSource tries to find default sync files
|
log.Printf("No input provided")
|
||||||
func startDefaultInputSource(instructions chan *LinkInstruction, status chan error) {
|
log.Printf("Provide input as: ")
|
||||||
if _, err := os.Stat("sync"); err == nil {
|
log.Printf("Arguments - %s <source>,<target>,<force?>", programName)
|
||||||
LogInfo("Using default sync file")
|
log.Printf("File - %s -f <file>", programName)
|
||||||
go ReadFromFile("sync", instructions, status, true)
|
log.Printf("Folder (finding sync files in folder recursively) - %s -r <folder>", programName)
|
||||||
} else if _, err := os.Stat("sync.yaml"); err == nil {
|
log.Printf("stdin - (cat <file> | %s)", programName)
|
||||||
LogInfo("Using default sync.yaml file")
|
os.Exit(1)
|
||||||
go ReadFromFile("sync.yaml", instructions, status, true)
|
}
|
||||||
} else if _, err := os.Stat("sync.yml"); err == nil {
|
}
|
||||||
LogInfo("Using default sync.yml file")
|
|
||||||
go ReadFromFile("sync.yml", instructions, status, true)
|
func ReadFromFilesRecursively(input string, output chan LinkInstruction, status chan error) {
|
||||||
} else {
|
defer close(output)
|
||||||
showUsageAndExit()
|
defer close(status)
|
||||||
}
|
|
||||||
}
|
input = NormalizePath(input)
|
||||||
|
log.Printf("Reading input from files recursively starting in %s%s%s", PathColor, input, DefaultColor)
|
||||||
// showUsageAndExit displays usage information and exits
|
|
||||||
func showUsageAndExit() {
|
files := make(chan string, 128)
|
||||||
LogInfo("No input provided")
|
recurseStatus := make(chan error)
|
||||||
LogInfo("Provide input as: ")
|
go GetSyncFilesRecursively(input, files, recurseStatus)
|
||||||
LogInfo("Arguments - %s <source>,<target>,<force?>", programName)
|
go func() {
|
||||||
LogInfo("File - %s -f <file>", programName)
|
for {
|
||||||
LogInfo("YAML File - %s -f <file.yaml>", programName)
|
err, ok := <-recurseStatus
|
||||||
LogInfo("Folder (finding sync files in folder recursively) - %s -r <folder>", programName)
|
if !ok {
|
||||||
LogInfo("stdin - (cat <file> | %s)", programName)
|
break
|
||||||
os.Exit(1)
|
}
|
||||||
}
|
if err != nil {
|
||||||
|
log.Printf("Failed to get sync files recursively: %s%+v%s", ErrorColor, err, DefaultColor)
|
||||||
// handleStatusErrors processes status channel errors
|
status <- err
|
||||||
func handleStatusErrors(status chan error) {
|
}
|
||||||
for {
|
}
|
||||||
err, ok := <-status
|
}()
|
||||||
if !ok {
|
|
||||||
break
|
var wg sync.WaitGroup
|
||||||
}
|
for {
|
||||||
if err != nil {
|
file, ok := <-files
|
||||||
LogError("%v", err)
|
if !ok {
|
||||||
}
|
log.Printf("No more files to process")
|
||||||
}
|
break
|
||||||
}
|
}
|
||||||
|
wg.Add(1)
|
||||||
// processInstructions processes all instructions from the channel using parallel workers
|
go func() {
|
||||||
func processInstructions(instructions chan *LinkInstruction) int32 {
|
defer wg.Done()
|
||||||
var instructionsDone int32 = 0
|
log.Println(file)
|
||||||
|
file = NormalizePath(file)
|
||||||
// Collect all instructions first
|
log.Printf("Processing file: %s%s%s", PathColor, file, DefaultColor)
|
||||||
var allInstructions []*LinkInstruction
|
|
||||||
for {
|
// This "has" to be done because instructions are resolved in relation to cwd
|
||||||
instruction, ok := <-instructions
|
fileDir := DirRegex.FindStringSubmatch(file)
|
||||||
if !ok {
|
if fileDir == nil {
|
||||||
LogInfo("No more instructions to process")
|
log.Printf("Failed to extract directory from %s%s%s", SourceColor, file, DefaultColor)
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
allInstructions = append(allInstructions, instruction)
|
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 {
|
||||||
// Process instructions in parallel using cyutils.WithWorkers
|
log.Printf("Failed to change directory to %s%s%s: %s%+v%s", SourceColor, fileDir[1], DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
// Let the library handle worker count - use 4 workers as a reasonable default
|
return
|
||||||
utils.WithWorkers(4, allInstructions, func(workerID int, _ int, instruction *LinkInstruction) {
|
}
|
||||||
LogInfo("Processing: %s", instruction.String())
|
|
||||||
status := make(chan error)
|
ReadFromFile(file, output, status, false)
|
||||||
go instruction.RunAsync(status)
|
}()
|
||||||
err := <-status
|
}
|
||||||
if err != nil {
|
wg.Wait()
|
||||||
LogError("Failed processing instruction: %v", err)
|
}
|
||||||
} else {
|
func ReadFromFile(input string, output chan LinkInstruction, status chan error, doclose bool) {
|
||||||
atomic.AddInt32(&instructionsDone, 1)
|
if doclose {
|
||||||
}
|
defer close(output)
|
||||||
})
|
defer close(status)
|
||||||
|
}
|
||||||
return instructionsDone
|
|
||||||
}
|
input = NormalizePath(input)
|
||||||
|
log.Printf("Reading input from file: %s%s%s", PathColor, input, DefaultColor)
|
||||||
func printSummary(fs FileSystem) {
|
file, err := os.Open(input)
|
||||||
lines := fs.SummaryLines()
|
if err != nil {
|
||||||
if len(lines) == 0 {
|
log.Fatalf("Failed to open file %s%s%s: %s%+v%s", SourceColor, input, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
LogInfo("Summary:")
|
defer file.Close()
|
||||||
for _, line := range lines {
|
|
||||||
LogInfo("%s", line)
|
scanner := bufio.NewScanner(file)
|
||||||
}
|
for scanner.Scan() {
|
||||||
}
|
line := scanner.Text()
|
||||||
|
instruction, err := ParseInstruction(line)
|
||||||
func IsPipeInput() bool {
|
if err != nil {
|
||||||
info, err := os.Stdin.Stat()
|
log.Printf("Error parsing line: %s'%s'%s, error: %s%+v%s", SourceColor, line, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
if err != nil {
|
continue
|
||||||
return false
|
}
|
||||||
}
|
log.Printf("Read instruction: %s", instruction.String())
|
||||||
return info.Mode()&os.ModeNamedPipe != 0
|
output <- instruction
|
||||||
}
|
}
|
||||||
|
}
|
||||||
func ReadFromFilesRecursively(input string, output chan *LinkInstruction, status chan error) {
|
func ReadFromArgs(output chan LinkInstruction, status chan error) {
|
||||||
defer close(output)
|
defer close(output)
|
||||||
defer close(status)
|
defer close(status)
|
||||||
|
|
||||||
workdir, _ := os.Getwd()
|
log.Printf("Reading input from args")
|
||||||
input = NormalizePath(input, workdir)
|
for _, arg := range os.Args[1:] {
|
||||||
LogInfo("Reading input from files recursively starting in %s", FormatPathValue(input))
|
instruction, err := ParseInstruction(arg)
|
||||||
|
if err != nil {
|
||||||
files := make(chan string, 128)
|
log.Printf("Error parsing arg: %s'%s'%s, error: %s%+v%s", SourceColor, arg, DefaultColor, ErrorColor, err, DefaultColor)
|
||||||
fileStatus := make(chan error)
|
continue
|
||||||
go GetSyncFilesRecursively(input, files, fileStatus)
|
}
|
||||||
|
output <- instruction
|
||||||
// Collect all files first
|
}
|
||||||
var syncFiles []string
|
}
|
||||||
for {
|
func ReadFromStdin(output chan LinkInstruction, status chan error) {
|
||||||
file, ok := <-files
|
defer close(output)
|
||||||
if !ok {
|
defer close(status)
|
||||||
break
|
|
||||||
}
|
log.Printf("Reading input from stdin")
|
||||||
syncFiles = append(syncFiles, file)
|
|
||||||
}
|
info, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
// Check for errors from file search
|
log.Fatalf("Failed to stat stdin: %s%+v%s", ErrorColor, err, DefaultColor)
|
||||||
for {
|
status <- err
|
||||||
err, ok := <-fileStatus
|
return
|
||||||
if !ok {
|
}
|
||||||
break
|
if info.Mode()&os.ModeNamedPipe != 0 {
|
||||||
}
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
if err != nil {
|
for scanner.Scan() {
|
||||||
LogError("Failed to get sync files recursively: %v", err)
|
line := scanner.Text()
|
||||||
status <- err
|
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
|
||||||
// Process each file
|
}
|
||||||
for _, file := range syncFiles {
|
output <- instruction
|
||||||
file = NormalizePath(file, workdir)
|
}
|
||||||
LogInfo("Processing file: %s", FormatPathValue(file))
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Fatalf("Error reading from stdin: %s%+v%s", ErrorColor, err, DefaultColor)
|
||||||
// Change to the directory containing the sync file
|
status <- err
|
||||||
fileDir := filepath.Dir(file)
|
return
|
||||||
originalDir, _ := os.Getwd()
|
}
|
||||||
err := os.Chdir(fileDir)
|
}
|
||||||
if err != nil {
|
}
|
||||||
LogError("Failed to change directory to %s: %v", FormatSourcePath(fileDir), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and process the file
|
|
||||||
ReadFromFile(file, output, status, false)
|
|
||||||
|
|
||||||
// Return to original directory
|
|
||||||
os.Chdir(originalDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadFromFile(input string, output chan *LinkInstruction, status chan error, doclose bool) {
|
|
||||||
if doclose {
|
|
||||||
defer close(output)
|
|
||||||
defer close(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
input = NormalizePath(input, filepath.Dir(input))
|
|
||||||
LogInfo("Reading input from file: %s", FormatPathValue(input))
|
|
||||||
|
|
||||||
// Check if this is a YAML file
|
|
||||||
if IsYAMLFile(input) {
|
|
||||||
LogInfo("Parsing as YAML file")
|
|
||||||
instructions, err := ParseYAMLFileRecursive(input, filepath.Dir(input))
|
|
||||||
if err != nil {
|
|
||||||
LogError("Failed to parse YAML file %s: %v",
|
|
||||||
FormatSourcePath(input), err)
|
|
||||||
status <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, instruction := range instructions {
|
|
||||||
instr := instruction // Create a copy to avoid reference issues
|
|
||||||
LogInfo("Read YAML instruction: %s", instr.String())
|
|
||||||
output <- &instr
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle CSV format (legacy)
|
|
||||||
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, 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadFromArgs(output chan *LinkInstruction, status chan error) {
|
|
||||||
defer close(output)
|
|
||||||
defer close(status)
|
|
||||||
|
|
||||||
workdir, _ := os.Getwd()
|
|
||||||
LogInfo("Reading input from args")
|
|
||||||
for _, arg := range flag.Args() {
|
|
||||||
instruction, err := ParseInstruction(arg, workdir)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Error parsing arg '%s': %v", arg, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
output <- &instruction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadFromStdin(output chan *LinkInstruction, status chan error) {
|
|
||||||
defer close(output)
|
|
||||||
defer close(status)
|
|
||||||
|
|
||||||
workdir, _ := os.Getwd()
|
|
||||||
LogInfo("Reading input from stdin")
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
instruction, err := ParseInstruction(line, workdir)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Error parsing line '%s': %v", line, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
output <- &instruction
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
LogError("Error reading from stdin: %v", err)
|
|
||||||
status <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
52
release.sh
52
release.sh
@@ -1,52 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "Figuring out the tag..."
|
|
||||||
TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
# Get the latest tag
|
|
||||||
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
|
||||||
# Increment the patch version
|
|
||||||
IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_TAG"
|
|
||||||
VERSION_PARTS[2]=$((VERSION_PARTS[2]+1))
|
|
||||||
TAG="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}"
|
|
||||||
# Create a new tag
|
|
||||||
git tag $TAG
|
|
||||||
git push origin $TAG
|
|
||||||
fi
|
|
||||||
echo "Tag: $TAG"
|
|
||||||
|
|
||||||
echo "Building the thing..."
|
|
||||||
sh build.sh
|
|
||||||
sh install.sh
|
|
||||||
|
|
||||||
echo "Creating a release..."
|
|
||||||
TOKEN="$GITEA_API_KEY"
|
|
||||||
GITEA="https://git.site.quack-lab.dev"
|
|
||||||
REPO="dave/synclib"
|
|
||||||
# Create a release
|
|
||||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"tag_name": "'"$TAG"'",
|
|
||||||
"name": "'"$TAG"'",
|
|
||||||
"draft": false,
|
|
||||||
"prerelease": false
|
|
||||||
}' \
|
|
||||||
$GITEA/api/v1/repos/$REPO/releases)
|
|
||||||
|
|
||||||
# Extract the release ID
|
|
||||||
echo $RELEASE_RESPONSE
|
|
||||||
RELEASE_ID=$(echo $RELEASE_RESPONSE | awk -F'"id":' '{print $2+0; exit}')
|
|
||||||
echo "Release ID: $RELEASE_ID"
|
|
||||||
|
|
||||||
echo "Uploading the things..."
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@cln.exe" \
|
|
||||||
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=cln.exe"
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@cln" \
|
|
||||||
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=cln"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
- source: A/**/*
|
|
||||||
target: B
|
|
||||||
- source: A/go.mod
|
|
||||||
target: B/go.mod
|
|
||||||
- source: A
|
|
||||||
target: B/foo
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 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
|
|
||||||
237
util.go
237
util.go
@@ -1,98 +1,139 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"log"
|
||||||
"path/filepath"
|
"os"
|
||||||
"strings"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
"sync"
|
||||||
)
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
func IsSymlink(path string) (bool, error) {
|
)
|
||||||
fileInfo, err := os.Lstat(path)
|
|
||||||
if err != nil {
|
func IsSymlink(path string) (bool, error) {
|
||||||
return false, err
|
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
|
// 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 FileExists(path string) bool {
|
||||||
}
|
_, err := os.Lstat(path)
|
||||||
|
return err == nil
|
||||||
func NormalizePath(input, workdir string) string {
|
}
|
||||||
input = filepath.Clean(input)
|
|
||||||
input = filepath.ToSlash(input)
|
func NormalizePath(input string) string {
|
||||||
input = strings.ReplaceAll(input, "\"", "")
|
workingdirectory, _ := os.Getwd()
|
||||||
|
input = strings.ReplaceAll(input, "\\", "/")
|
||||||
if !filepath.IsAbs(input) {
|
input = strings.ReplaceAll(input, "\"", "")
|
||||||
LogInfo("Input '%s' is not absolute, prepending work dir '%s'", input, workdir)
|
|
||||||
var err error
|
if !filepath.IsAbs(input) {
|
||||||
input = filepath.Join(workdir, input)
|
input = workingdirectory + "/" + input
|
||||||
input, err = filepath.Abs(input)
|
}
|
||||||
if err != nil {
|
|
||||||
LogError("Failed to get absolute path for %s: %v", FormatSourcePath(input), err)
|
return filepath.Clean(input)
|
||||||
return input
|
}
|
||||||
}
|
|
||||||
}
|
func AreSame(lhs string, rhs string) bool {
|
||||||
|
lhsinfo, err := os.Stat(lhs)
|
||||||
input = filepath.Clean(input)
|
if err != nil {
|
||||||
input = filepath.ToSlash(input)
|
return false
|
||||||
return input
|
}
|
||||||
}
|
rhsinfo, err := os.Stat(rhs)
|
||||||
|
if err != nil {
|
||||||
func AreSame(lhs string, rhs string) bool {
|
return false
|
||||||
lhsinfo, err := os.Stat(lhs)
|
}
|
||||||
if err != nil {
|
|
||||||
return false
|
return os.SameFile(lhsinfo, rhsinfo)
|
||||||
}
|
}
|
||||||
rhsinfo, err := os.Stat(rhs)
|
|
||||||
if err != nil {
|
func ConvertHome(input string) (string, error) {
|
||||||
return false
|
if strings.Contains(input, "~") {
|
||||||
}
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
return os.SameFile(lhsinfo, rhsinfo)
|
return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertHome(input string) (string, error) {
|
return strings.Replace(input, "~", homedir, 1), nil
|
||||||
if strings.HasPrefix(input, "~/") {
|
}
|
||||||
homedir, err := os.UserHomeDir()
|
return input, nil
|
||||||
if err != nil {
|
}
|
||||||
return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err)
|
|
||||||
}
|
func GetSyncFilesRecursively(input string, output chan string, status chan error) {
|
||||||
|
defer close(output)
|
||||||
return strings.Replace(input, "~", homedir, 1), nil
|
defer close(status)
|
||||||
}
|
|
||||||
return input, nil
|
var filesProcessed int32
|
||||||
}
|
var foldersProcessed int32
|
||||||
|
progressTicker := time.NewTicker(200 * time.Millisecond)
|
||||||
func GetSyncFilesRecursively(input string, output chan string, status chan error) {
|
defer progressTicker.Stop()
|
||||||
defer close(output)
|
|
||||||
defer close(status)
|
var wg sync.WaitGroup
|
||||||
|
var initial sync.Once
|
||||||
workdir, _ := os.Getwd()
|
wg.Add(1)
|
||||||
input = NormalizePath(input, workdir)
|
directories := make(chan string, 100000)
|
||||||
LogInfo("Searching for sync files recursively starting in %s", FormatPathValue(input))
|
workerPool := make(chan struct{}, 4000)
|
||||||
|
directories <- input
|
||||||
// Use doublestar to find all sync.yml and sync.yaml files recursively
|
|
||||||
pattern := "**/sync.y*ml"
|
go func() {
|
||||||
files, err := doublestar.Glob(os.DirFS(input), pattern)
|
for {
|
||||||
if err != nil {
|
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", filesProcessed, foldersProcessed, len(workerPool), len(directories))
|
||||||
LogError("Failed to search for pattern %s: %v", pattern, err)
|
<-progressTicker.C
|
||||||
status <- err
|
}
|
||||||
return
|
}()
|
||||||
}
|
|
||||||
|
log.Printf("%+v", len(workerPool))
|
||||||
for _, file := range files {
|
go func() {
|
||||||
fullPath := filepath.Join(input, file)
|
for directory := range directories {
|
||||||
LogInfo("Found sync file: %s", FormatPathValue(fullPath))
|
workerPool <- struct{}{}
|
||||||
output <- fullPath
|
wg.Add(1)
|
||||||
}
|
go func(directory string) {
|
||||||
|
atomic.AddInt32(&foldersProcessed, 1)
|
||||||
LogInfo("Completed recursive search for sync files")
|
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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 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...
|
||||||
|
wg.Wait()
|
||||||
|
log.Printf("Files processed: %d; Folders processed: %d", filesProcessed, foldersProcessed)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user