Compare commits
	
		
			21 Commits
		
	
	
		
			c94a7ae8ab
			...
			v1.5.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ff76a5399c | |||
| 3f0791466b | |||
| dc5eb9cb80 | |||
| 25a8e2b65a | |||
| 59faaa181d | |||
| 7bff91679d | |||
| cfa7fc73c9 | |||
| a568a736aa | |||
| db72688aa2 | |||
| 05082d8ff3 | |||
| a4f90c2bc8 | |||
| bec5b3cb9c | |||
| 018c0797f5 | |||
| a7d5317114 | |||
| 89e29eacee | |||
| 4e4e58af83 | |||
| eb81ec4162 | |||
| 21d7f56ccf | |||
| 8653191df2 | |||
| 9da47ce0cf | |||
| 29bfa2d776 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,3 +4,5 @@ cln | |||||||
| cln.log | cln.log | ||||||
| .qodo | .qodo | ||||||
| *.log | *.log | ||||||
|  | *.out | ||||||
|  | test_temp | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								build.sh
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								build.sh
									
									
									
									
									
								
							| @@ -1,2 +1,2 @@ | |||||||
| GOOS=windows GOARCH=amd64 go build -o cln.exe . | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o cln.exe . | ||||||
| GOOS=linux GOARCH=amd64 go build -o cln . | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o cln . | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,7 +1,17 @@ | |||||||
| module cln | module cln | ||||||
|  |  | ||||||
| go 1.21.7 | go 1.23.6 | ||||||
|  |  | ||||||
| require gopkg.in/yaml.v3 v3.0.1 | require gopkg.in/yaml.v3 v3.0.1 | ||||||
|  |  | ||||||
| require github.com/bmatcuk/doublestar/v4 v4.8.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 | ||||||
|  | ) | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,5 +1,15 @@ | |||||||
|  | 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 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= | ||||||
| github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= | 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
|   | |||||||
							
								
								
									
										179
									
								
								instruction.go
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								instruction.go
									
									
									
									
									
								
							| @@ -20,6 +20,7 @@ type LinkInstruction struct { | |||||||
|  |  | ||||||
| type YAMLConfig struct { | type YAMLConfig struct { | ||||||
| 	Links []LinkInstruction `yaml:"links"` | 	Links []LinkInstruction `yaml:"links"` | ||||||
|  | 	From  []string          `yaml:"from,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func (instruction *LinkInstruction) Tidy() { | func (instruction *LinkInstruction) Tidy() { | ||||||
| @@ -269,40 +270,24 @@ func ParseYAMLFile(filename, workdir string) ([]LinkInstruction, error) { | |||||||
| 		return nil, fmt.Errorf("error reading YAML file: %w", err) | 		return nil, fmt.Errorf("error reading YAML file: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// First try to parse as a list of link instructions | 	// Parse as a direct list of instructions | ||||||
| 	var config YAMLConfig | 	var instructions []LinkInstruction | ||||||
| 	err = yaml.Unmarshal(data, &config) | 	err = yaml.Unmarshal(data, &instructions) | ||||||
| 	if err != nil || len(config.Links) == 0 { | 	if err != nil { | ||||||
| 		// If that fails, try parsing as a direct list of instructions | 		return nil, fmt.Errorf("error parsing YAML: %w", err) | ||||||
| 		var instructions []LinkInstruction |  | ||||||
| 		err = yaml.Unmarshal(data, &instructions) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("error parsing YAML: %w", err) |  | ||||||
| 		} |  | ||||||
| 		config.Links = instructions |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	expanded := []LinkInstruction{} | 	// Preprocess instructions: expand globs and from references | ||||||
| 	for _, link := range config.Links { | 	// Create a new visited map for this file | ||||||
| 		LogSource("Expanding pattern source %s in YAML file %s", link.Source, filename) | 	visited := make(map[string]bool) | ||||||
| 		newlinks, err := ExpandPattern(link.Source, workdir, link.Target) | 	processedInstructions, err := preprocessInstructions(instructions, filename, workdir, visited) | ||||||
| 		if err != nil { | 	if err != nil { | ||||||
| 			return nil, fmt.Errorf("error expanding pattern: %w", err) | 		return nil, err | ||||||
| 		} |  | ||||||
| 		// "Clone" the original link instruction for each expanded link |  | ||||||
| 		for i := range newlinks { |  | ||||||
| 			newlinks[i].Delete = link.Delete |  | ||||||
| 			newlinks[i].Hard = link.Hard |  | ||||||
| 			newlinks[i].Force = link.Force |  | ||||||
| 		} |  | ||||||
| 		LogInfo("Expanded pattern %s in YAML file %s to %d links", |  | ||||||
| 			FormatSourcePath(link.Source), FormatSourcePath(filename), len(newlinks)) |  | ||||||
| 		expanded = append(expanded, newlinks...) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i := range expanded { | 	// Final processing: normalize paths and set defaults | ||||||
| 		link := &expanded[i] | 	for i := range processedInstructions { | ||||||
|  | 		link := &processedInstructions[i] | ||||||
| 		link.Tidy() | 		link.Tidy() | ||||||
| 		link.Source, _ = ConvertHome(link.Source) | 		link.Source, _ = ConvertHome(link.Source) | ||||||
| 		link.Target, _ = ConvertHome(link.Target) | 		link.Target, _ = ConvertHome(link.Target) | ||||||
| @@ -315,12 +300,142 @@ func ParseYAMLFile(filename, workdir string) ([]LinkInstruction, error) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return expanded, nil | 	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 { | ||||||
|  | 				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 | ||||||
|  | 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) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// 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) | ||||||
|  | 	return parseYAMLFileRecursive(fromPath, fromWorkdir, visited) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // expandGlobs expands glob patterns in a single instruction | ||||||
|  | func expandGlobs(instr LinkInstruction, filename, workdir string) ([]LinkInstruction, error) { | ||||||
|  | 	LogSource("Expanding pattern source %s in YAML file %s", instr.Source, filename) | ||||||
|  | 	newlinks, err := ExpandPattern(instr.Source, workdir, instr.Target) | ||||||
|  | 	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 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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) (links []LinkInstruction, err error) { | func ExpandPattern(source, workdir, target string) (links []LinkInstruction, err error) { | ||||||
| 	static, pattern := doublestar.SplitPattern(source) | 	static, pattern := doublestar.SplitPattern(source) | ||||||
| 	if static == "" { | 	if static == "" || static == "." { | ||||||
| 		static = workdir | 		static = workdir | ||||||
| 	} | 	} | ||||||
| 	LogInfo("Static part: %s", static) | 	LogInfo("Static part: %s", static) | ||||||
|   | |||||||
							
								
								
									
										4441
									
								
								instruction_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4441
									
								
								instruction_test.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										143
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								main.go
									
									
									
									
									
								
							| @@ -7,8 +7,8 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"sync" |  | ||||||
| 	"sync/atomic" | 	"sync/atomic" | ||||||
|  | 	utils "git.site.quack-lab.dev/dave/cyutils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const deliminer = "," | const deliminer = "," | ||||||
| @@ -30,7 +30,27 @@ func main() { | |||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 	undo = *undoF | 	undo = *undoF | ||||||
|  |  | ||||||
| 	if *debug { | 	setupLogging(*debug) | ||||||
|  |  | ||||||
|  | 	instructions := make(chan *LinkInstruction, 1000) | ||||||
|  | 	status := make(chan error) | ||||||
|  |  | ||||||
|  | 	startInputSource(*recurse, *file, instructions, status) | ||||||
|  |  | ||||||
|  | 	go handleStatusErrors(status) | ||||||
|  |  | ||||||
|  | 	instructionsDone := processInstructions(instructions) | ||||||
|  |  | ||||||
|  | 	if instructionsDone == 0 { | ||||||
|  | 		LogInfo("No instructions were processed") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	LogInfo("All done") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // setupLogging configures logging based on debug flag | ||||||
|  | func setupLogging(debug bool) { | ||||||
|  | 	if debug { | ||||||
| 		log.SetFlags(log.Lmicroseconds | log.Lshortfile) | 		log.SetFlags(log.Lmicroseconds | log.Lshortfile) | ||||||
| 		logFile, err := os.Create(programName + ".log") | 		logFile, err := os.Create(programName + ".log") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -42,19 +62,19 @@ func main() { | |||||||
| 	} else { | 	} else { | ||||||
| 		log.SetFlags(log.Lmicroseconds) | 		log.SetFlags(log.Lmicroseconds) | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| 	instructions := make(chan *LinkInstruction, 1000) | // startInputSource determines and starts the appropriate input source | ||||||
| 	status := make(chan error) | func startInputSource(recurse, file string, instructions chan *LinkInstruction, status chan error) { | ||||||
|  |  | ||||||
| 	// Check input sources in priority order | 	// Check input sources in priority order | ||||||
| 	switch { | 	switch { | ||||||
| 	case *recurse != "": | 	case recurse != "": | ||||||
| 		LogInfo("Recurse: %s", *recurse) | 		LogInfo("Recurse: %s", recurse) | ||||||
| 		go ReadFromFilesRecursively(*recurse, instructions, status) | 		go ReadFromFilesRecursively(recurse, instructions, status) | ||||||
|  |  | ||||||
| 	case *file != "": | 	case file != "": | ||||||
| 		LogInfo("File: %s", *file) | 		LogInfo("File: %s", file) | ||||||
| 		go ReadFromFile(*file, instructions, status, true) | 		go ReadFromFile(file, instructions, status, true) | ||||||
|  |  | ||||||
| 	case len(flag.Args()) > 0: | 	case len(flag.Args()) > 0: | ||||||
| 		LogInfo("Reading from command line arguments") | 		LogInfo("Reading from command line arguments") | ||||||
| @@ -65,64 +85,81 @@ func main() { | |||||||
| 	// 	go ReadFromStdin(instructions, status) | 	// 	go ReadFromStdin(instructions, status) | ||||||
|  |  | ||||||
| 	default: | 	default: | ||||||
| 		if _, err := os.Stat("sync"); err == nil { | 		startDefaultInputSource(instructions, status) | ||||||
| 			LogInfo("Using default sync file") | 	} | ||||||
| 			go ReadFromFile("sync", instructions, status, true) | } | ||||||
| 		} else if _, err := os.Stat("sync.yaml"); err == nil { |  | ||||||
| 			LogInfo("Using default sync.yaml file") | // startDefaultInputSource tries to find default sync files | ||||||
| 			go ReadFromFile("sync.yaml", instructions, status, true) | func startDefaultInputSource(instructions chan *LinkInstruction, status chan error) { | ||||||
| 		} else if _, err := os.Stat("sync.yml"); err == nil { | 	if _, err := os.Stat("sync"); err == nil { | ||||||
| 			LogInfo("Using default sync.yml file") | 		LogInfo("Using default sync file") | ||||||
| 			go ReadFromFile("sync.yml", instructions, status, true) | 		go ReadFromFile("sync", instructions, status, true) | ||||||
| 		} else { | 	} else if _, err := os.Stat("sync.yaml"); err == nil { | ||||||
| 			LogInfo("No input provided") | 		LogInfo("Using default sync.yaml file") | ||||||
| 			LogInfo("Provide input as: ") | 		go ReadFromFile("sync.yaml", instructions, status, true) | ||||||
| 			LogInfo("Arguments - %s <source>,<target>,<force?>", programName) | 	} else if _, err := os.Stat("sync.yml"); err == nil { | ||||||
| 			LogInfo("File - %s -f <file>", programName) | 		LogInfo("Using default sync.yml file") | ||||||
| 			LogInfo("YAML File - %s -f <file.yaml>", programName) | 		go ReadFromFile("sync.yml", instructions, status, true) | ||||||
| 			LogInfo("Folder (finding sync files in folder recursively) - %s -r <folder>", programName) | 	} else { | ||||||
| 			LogInfo("stdin - (cat <file> | %s)", programName) | 		showUsageAndExit() | ||||||
| 			os.Exit(1) | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // showUsageAndExit displays usage information and exits | ||||||
|  | func showUsageAndExit() { | ||||||
|  | 	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) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleStatusErrors processes status channel errors | ||||||
|  | func handleStatusErrors(status chan error) { | ||||||
|  | 	for { | ||||||
|  | 		err, ok := <-status | ||||||
|  | 		if !ok { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		if err != nil { | ||||||
|  | 			LogError("%v", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| 	go func() { | // processInstructions processes all instructions from the channel using parallel workers | ||||||
| 		for { | func processInstructions(instructions chan *LinkInstruction) int32 { | ||||||
| 			err, ok := <-status |  | ||||||
| 			if !ok { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 			if err != nil { |  | ||||||
| 				LogError("%v", err) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	var instructionsDone int32 = 0 | 	var instructionsDone int32 = 0 | ||||||
| 	var wg sync.WaitGroup |  | ||||||
|  | 	// Collect all instructions first | ||||||
|  | 	var allInstructions []*LinkInstruction | ||||||
| 	for { | 	for { | ||||||
| 		instruction, ok := <-instructions | 		instruction, ok := <-instructions | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			LogInfo("No more instructions to process") | 			LogInfo("No more instructions to process") | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
|  | 		allInstructions = append(allInstructions, instruction) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Process instructions in parallel using cyutils.WithWorkers | ||||||
|  | 	// Let the library handle worker count - use 4 workers as a reasonable default | ||||||
|  | 	utils.WithWorkers(4, allInstructions, func(workerID int, _ int, instruction *LinkInstruction) { | ||||||
| 		LogInfo("Processing: %s", instruction.String()) | 		LogInfo("Processing: %s", instruction.String()) | ||||||
| 		status := make(chan error) | 		status := make(chan error) | ||||||
| 		go instruction.RunAsync(status) | 		go instruction.RunAsync(status) | ||||||
| 		wg.Add(1) |  | ||||||
| 		err := <-status | 		err := <-status | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			LogError("Failed processing instruction: %v", err) | 			LogError("Failed processing instruction: %v", err) | ||||||
|  | 		} else { | ||||||
|  | 			atomic.AddInt32(&instructionsDone, 1) | ||||||
| 		} | 		} | ||||||
| 		atomic.AddInt32(&instructionsDone, 1) | 	}) | ||||||
| 		wg.Done() |  | ||||||
| 	} | 	return instructionsDone | ||||||
| 	wg.Wait() |  | ||||||
| 	if instructionsDone == 0 { |  | ||||||
| 		LogInfo("No instructions were processed") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	LogInfo("All done") |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func IsPipeInput() bool { | func IsPipeInput() bool { | ||||||
| @@ -201,7 +238,7 @@ func ReadFromFile(input string, output chan *LinkInstruction, status chan error, | |||||||
| 	// Check if this is a YAML file | 	// Check if this is a YAML file | ||||||
| 	if IsYAMLFile(input) { | 	if IsYAMLFile(input) { | ||||||
| 		LogInfo("Parsing as YAML file") | 		LogInfo("Parsing as YAML file") | ||||||
| 		instructions, err := ParseYAMLFile(input, filepath.Dir(input)) | 		instructions, err := ParseYAMLFileRecursive(input, filepath.Dir(input)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			LogError("Failed to parse YAML file %s: %v", | 			LogError("Failed to parse YAML file %s: %v", | ||||||
| 				FormatSourcePath(input), err) | 				FormatSourcePath(input), err) | ||||||
|   | |||||||
| @@ -46,3 +46,7 @@ curl -X POST \ | |||||||
|   -H "Authorization: token $TOKEN" \ |   -H "Authorization: token $TOKEN" \ | ||||||
|   -F "attachment=@cln.exe" \ |   -F "attachment=@cln.exe" \ | ||||||
|   "$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=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" | ||||||
		Reference in New Issue
	
	Block a user