1 Commits

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

6
.gitignore vendored
View File

@@ -1,4 +1,2 @@
*.exe
*.exe
cln
cln.log
main.exe

View File

@@ -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

View File

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

View File

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

4
go.mod
View File

@@ -1,5 +1,3 @@
module cln
module main
go 1.21.7
require gopkg.in/yaml.v3 v3.0.1

4
go.sum
View File

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

View File

@@ -2,65 +2,24 @@ package main
import (
"fmt"
"log"
"os"
"path/filepath"
"slices"
"regexp"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
type LinkInstruction struct {
Source string `yaml:"source"`
Target string `yaml:"target"`
Force bool `yaml:"force,omitempty"`
Hard bool `yaml:"hard,omitempty"`
Delete bool `yaml:"delete,omitempty"`
}
type YAMLConfig struct {
Links []LinkInstruction `yaml:"links"`
}
func (instruction *LinkInstruction) Tidy() {
instruction.Source = strings.ReplaceAll(instruction.Source, "\"", "")
instruction.Source = strings.ReplaceAll(instruction.Source, "\\", "/")
instruction.Source = strings.TrimSpace(instruction.Source)
instruction.Target = strings.ReplaceAll(instruction.Target, "\"", "")
instruction.Target = strings.ReplaceAll(instruction.Target, "\\", "/")
instruction.Target = strings.TrimSpace(instruction.Target)
Source string
Target string
Force bool
}
func (instruction *LinkInstruction) String() string {
var flags []string
if instruction.Force {
flags = append(flags, "force=true")
}
if instruction.Hard {
flags = append(flags, "hard=true")
}
if instruction.Delete {
flags = append(flags, "delete=true")
}
flagsStr := ""
if len(flags) > 0 {
flagsStr = " [" + strings.Join(flags, ", ") + "]"
}
return fmt.Sprintf("%s%s%s → %s%s%s%s",
SourceColor, instruction.Source, DefaultColor,
TargetColor, instruction.Target, DefaultColor,
flagsStr)
}
func ParseInstruction(line, workdir string) (LinkInstruction, error) {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
return LinkInstruction{}, fmt.Errorf("comment line")
return fmt.Sprintf("%s%s%s%s%s%s%s%s%s", SourceColor, instruction.Source, DefaultColor, deliminer, TargetColor, instruction.Target, DefaultColor, deliminer, strconv.FormatBool(instruction.Force))
}
func ParseInstruction(line string) (LinkInstruction, error) {
parts := strings.Split(line, deliminer)
instruction := LinkInstruction{}
@@ -68,78 +27,72 @@ func ParseInstruction(line, workdir string) (LinkInstruction, error) {
return instruction, fmt.Errorf("invalid format - not enough parameters (must have at least source and target)")
}
instruction.Source = strings.TrimSpace(parts[0])
instruction.Target = strings.TrimSpace(parts[1])
for i := 2; i < len(parts); i++ {
flagPart := strings.TrimSpace(parts[i])
// Support for legacy format (backward compatibility)
if !strings.Contains(flagPart, "=") {
// Legacy format: positional boolean flags
switch i {
case 2: // Force flag (3rd position)
instruction.Force = isTrue(flagPart)
case 3: // Hard flag (4th position)
instruction.Hard = isTrue(flagPart)
case 4: // Delete flag (5th position)
instruction.Delete = isTrue(flagPart)
if instruction.Delete {
instruction.Force = true // Delete implies Force
}
}
continue
instruction.Source = parts[0]
instruction.Target = parts[1]
instruction.Force = false
if len(parts) > 2 {
res, _ := regexp.MatchString("^(?i)T|TRUE$", parts[2])
instruction.Force = res
}
// 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
}
}
}
instruction.Tidy()
instruction.Source, _ = ConvertHome(instruction.Source)
instruction.Target, _ = ConvertHome(instruction.Target)
instruction.Source = NormalizePath(instruction.Source, workdir)
instruction.Target = NormalizePath(instruction.Target, workdir)
instruction.Source = NormalizePath(instruction.Source)
instruction.Target = NormalizePath(instruction.Target)
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) RunSync() error {
if !FileExists(instruction.Source) {
return fmt.Errorf("instruction source %s%s%s does not exist", SourceColor, instruction.Source, DefaultColor)
}
if AreSame(instruction.Source, instruction.Target) {
log.Printf("Source %s%s%s and target %s%s%s are the same, %snothing to do...%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, PathColor, DefaultColor)
return nil
}
if FileExists(instruction.Target) {
if instruction.Force {
isSymlink, err := IsSymlink(instruction.Target)
if err != nil {
return fmt.Errorf("could not determine whether %s%s%s is a sym link or not, stopping; err: %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
}
if isSymlink {
log.Printf("Removing symlink at %s%s%s", TargetColor, instruction.Target, DefaultColor)
err = os.Remove(instruction.Target)
if err != nil {
return fmt.Errorf("failed deleting %s%s%s due to %s%+v%s", TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
}
} else {
return fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
}
} else {
return fmt.Errorf("target %s%s%s exists - handle manually or set the 'forced' flag (3rd field)", TargetColor, instruction.Target, DefaultColor)
}
}
err := os.Symlink(instruction.Source, instruction.Target)
if err != nil {
return fmt.Errorf("failed creating symlink between %s%s%s and %s%s%s with error %s%+v%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, ErrorColor, err, DefaultColor)
}
log.Printf("Created symlink between %s%s%s and %s%s%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor)
return nil
}
func (instruction *LinkInstruction) RunAsync(status chan (error)) {
defer close(status)
if !FileExists(instruction.Source) {
status <- fmt.Errorf("instruction source %s does not exist", FormatSourcePath(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 and target %s are the same, nothing to do...",
FormatSourcePath(instruction.Source),
FormatTargetPath(instruction.Target))
if AreSame(instruction.Source, instruction.Target) {
status <- fmt.Errorf("source %s%s%s and target %s%s%s are the same, %snothing to do...%s", SourceColor, instruction.Source, DefaultColor, TargetColor, instruction.Target, DefaultColor, PathColor, DefaultColor)
return
}
@@ -147,166 +100,33 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
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)
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, 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 := os.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)
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 due to %v",
FormatTargetPath(instruction.Target), err)
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",
FormatTargetPath(instruction.Target))
status <- fmt.Errorf("refusing to delte actual (non symlink) file %s%s%s", TargetColor, instruction.Target, DefaultColor)
return
}
LogImportant("Deleting (!!!) %s", instruction.Target)
err = os.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))
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)
err := os.Symlink(instruction.Source, instruction.Target)
if err != nil {
status <- fmt.Errorf("failed creating directory %s due to %v",
FormatTargetPath(targetDir), err)
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
}
}
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 and %s with error %v",
FormatSourcePath(instruction.Source),
FormatTargetPath(instruction.Target),
err)
return
}
LogInfo("Created symlink between %s and %s",
FormatSourcePath(instruction.Source),
FormatTargetPath(instruction.Target))
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
}
toRemove := []int{}
for i, link := range config.Links {
if strings.Contains(link.Source, "*") {
LogSource("Expanding wildcard source %s in YAML file %s", link.Source, filename)
newlinks, err := ExpandWildcard(link.Source, workdir, link.Target)
if err != nil {
return nil, fmt.Errorf("error expanding wildcard: %w", err)
}
LogInfo("Expanded wildcard source %s in YAML file %s to %d links",
FormatSourcePath(link.Source), FormatSourcePath(filename), len(newlinks))
config.Links = append(config.Links, newlinks...)
toRemove = append(toRemove, i)
}
}
for i := len(toRemove) - 1; i >= 0; i-- {
config.Links = slices.Delete(config.Links, toRemove[i], 1)
}
for i := range config.Links {
link := &config.Links[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 config.Links[i].Delete {
config.Links[i].Force = true
}
}
return config.Links, nil
}
func ExpandWildcard(source, workdir, target string) (links []LinkInstruction, err error) {
dir := filepath.Dir(source)
pattern := filepath.Base(source)
files, err := filepath.Glob(filepath.Join(workdir, dir, pattern))
if err != nil {
return nil, fmt.Errorf("error expanding wildcard: %w", err)
}
for _, file := range files {
link := LinkInstruction{
Source: file,
Target: filepath.Join(target, filepath.Base(file)),
}
links = append(links, link)
}
LogInfo("Expanded wildcard source %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"
}

View File

@@ -1,92 +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"
)
// 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)
}
// 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...)
}

234
main.go
View File

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

1
sync Normal file
View File

@@ -0,0 +1 @@
test,testdir/test3

View File

@@ -1,2 +0,0 @@
- source: \*
target: test/

View File

@@ -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

View File

@@ -1,26 +0,0 @@
18:24:37.967093 Using default sync.yaml file
18:24:37.967093 Input 'sync.yaml' is not absolute, prepending work dir '.'
18:24:37.967593 Reading input from file: C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml
18:24:37.967593 Parsing as YAML file
18:24:37.967593 Failed to parse YAML file C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml: error unmarshaling YAML: yaml: unmarshal errors:
line 1: cannot unmarshal !!seq into main.YAMLConfig
18:24:37.967593 No more instructions to process
18:24:37.968092 No instructions were processed
18:27:59.691333 Using default sync.yaml file
18:27:59.691333 Input 'sync.yaml' is not absolute, prepending work dir '.'
18:27:59.691834 Reading input from file: C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml
18:27:59.691834 Parsing as YAML file
18:27:59.692335 Expanding wildcard source \* in YAML file C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml
18:27:59.692335 Expanded wildcard source \* to 0 links
18:27:59.692836 Expanded wildcard source \* in YAML file C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml to 0 links
18:27:59.692836 No more instructions to process
18:27:59.692836 No instructions were processed
18:28:04.075821 Using default sync.yaml file
18:28:04.076320 Input 'sync.yaml' is not absolute, prepending work dir '.'
18:28:04.076320 Reading input from file: C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml
18:28:04.076320 Parsing as YAML file
18:28:04.076320 Expanding wildcard source \* in YAML file C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml
18:28:04.076821 Expanded wildcard source \* to 0 links
18:28:04.076821 Expanded wildcard source \* in YAML file C:/Users/Administrator/Seafile/Projects-Go/GoProjects/synclib/sync.yaml to 0 links
18:28:04.076821 No more instructions to process
18:28:04.076821 No instructions were processed

134
util.go
View File

@@ -2,6 +2,7 @@ package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
@@ -26,25 +27,16 @@ func FileExists(path string) bool {
return err == nil
}
func NormalizePath(input, workdir string) string {
input = filepath.Clean(input)
input = filepath.ToSlash(input)
func NormalizePath(input string) string {
workingdirectory, _ := os.Getwd()
input = strings.ReplaceAll(input, "\\", "/")
input = strings.ReplaceAll(input, "\"", "")
if !filepath.IsAbs(input) {
LogInfo("Input '%s' is not absolute, prepending work dir '%s'", input, workdir)
var err error
input = filepath.Join(workdir, input)
input, err = filepath.Abs(input)
if err != nil {
LogError("Failed to get absolute path for %s: %v", FormatSourcePath(input), err)
return input
}
input = workingdirectory + "/" + input
}
input = filepath.Clean(input)
input = filepath.ToSlash(input)
return input
return filepath.Clean(input)
}
func AreSame(lhs string, rhs string) bool {
@@ -61,7 +53,7 @@ func AreSame(lhs string, rhs string) bool {
}
func ConvertHome(input string) (string, error) {
if strings.HasPrefix(input, "~/") {
if strings.Contains(input, "~") {
homedir, err := os.UserHomeDir()
if err != nil {
return input, fmt.Errorf("unable to convert ~ to user directory with error %+v", err)
@@ -78,110 +70,70 @@ func GetSyncFilesRecursively(input string, output chan string, status chan error
var filesProcessed int32
var foldersProcessed int32
var activeWorkers int32
progressTicker := time.NewTicker(200 * time.Millisecond)
defer progressTicker.Stop()
done := make(chan struct{})
defer close(done)
var wg sync.WaitGroup
var initial sync.Once
wg.Add(1)
directories := make(chan string, 100000)
workerPool := make(chan struct{}, 4000)
directories <- input
go func() {
for {
select {
case <-progressTicker.C:
dirCount := len(directories)
workers := atomic.LoadInt32(&activeWorkers)
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Active workers: %d; Directory queue: %d",
atomic.LoadInt32(&filesProcessed),
atomic.LoadInt32(&foldersProcessed),
workers,
dirCount)
case <-done:
// Final progress update
fmt.Printf("\nFiles processed: %d; Folders processed: %d; Completed successfully\n",
atomic.LoadInt32(&filesProcessed),
atomic.LoadInt32(&foldersProcessed))
return
}
fmt.Printf("\rFiles processed: %d; Folders processed: %d; Workers: %d; Directory Stack Size: %d;", filesProcessed, foldersProcessed, len(workerPool), len(directories))
<-progressTicker.C
}
}()
allDone := make(chan struct{})
log.Printf("%+v", len(workerPool))
go func() {
// WTF is this waitgroup?
// Nowhere is it added...
var wg sync.WaitGroup
go func() {
for {
if atomic.LoadInt32(&activeWorkers) == 0 && len(directories) == 0 {
time.Sleep(10 * time.Millisecond)
if atomic.LoadInt32(&activeWorkers) == 0 && len(directories) == 0 {
close(allDone)
return
}
}
time.Sleep(50 * time.Millisecond)
}
}()
for {
select {
case directory, ok := <-directories:
if !ok {
wg.Wait()
return
}
atomic.AddInt32(&activeWorkers, 1)
go func(dir string) {
for directory := range directories {
workerPool <- struct{}{}
wg.Add(1)
go func(directory string) {
atomic.AddInt32(&foldersProcessed, 1)
processDirectory(dir, directories, output, &filesProcessed)
defer wg.Done()
defer func() { <-workerPool }()
<-workerPool
atomic.AddInt32(&activeWorkers, -1)
}(directory)
}
}
}()
<-allDone
if atomic.LoadInt32(&filesProcessed) > 0 {
LogInfo("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 {
LogError("Error reading directory %s: %v", directory, err)
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 {
if IsYAMLSyncFile(file.Name()) {
// 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)
}
atomic.AddInt32(&filesProcessed, 1)
}
}
// log.Printf("Done reading directory %s", directory)
func IsYAMLSyncFile(filename string) bool {
return filename == "sync.yaml" || filename == "sync.yml"
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)
}