diff --git a/backup/cmd/list.go b/backup/cmd/list.go index c0e8d4a..2e957a3 100644 --- a/backup/cmd/list.go +++ b/backup/cmd/list.go @@ -2,6 +2,8 @@ package cmd import ( "backup-rsync/backup/internal" + "io" + "log" "github.com/spf13/cobra" ) @@ -16,7 +18,8 @@ func buildListCommand() *cobra.Command { cfg := internal.LoadResolvedConfig(configPath) command := internal.NewListCommand(rsyncPath) - cfg.Apply(command) + logger := log.New(io.Discard, "", 0) + cfg.Apply(command, logger) }, } } diff --git a/backup/cmd/run.go b/backup/cmd/run.go index 4c5f9fe..2cbd603 100644 --- a/backup/cmd/run.go +++ b/backup/cmd/run.go @@ -15,9 +15,10 @@ func buildRunCommand() *cobra.Command { rsyncPath, _ := cmd.Flags().GetString("rsync-path") cfg := internal.LoadResolvedConfig(configPath) - command := internal.NewRSyncCommand(rsyncPath) + logger, logPath := internal.CreateMainLogger() + command := internal.NewSyncCommand(rsyncPath, logPath) - cfg.Apply(command) + cfg.Apply(command, logger) }, } } diff --git a/backup/cmd/simulate.go b/backup/cmd/simulate.go index 42dd756..d03e694 100644 --- a/backup/cmd/simulate.go +++ b/backup/cmd/simulate.go @@ -15,9 +15,10 @@ func buildSimulateCommand() *cobra.Command { rsyncPath, _ := cmd.Flags().GetString("rsync-path") cfg := internal.LoadResolvedConfig(configPath) - command := internal.NewRSyncSimulateCommand(rsyncPath) + logger, logPath := internal.CreateMainLogger() + command := internal.NewSimulateCommand(rsyncPath, logPath) - cfg.Apply(command) + cfg.Apply(command, logger) }, } } diff --git a/backup/cmd/version.go b/backup/cmd/version.go index f5948fa..e6e4c69 100644 --- a/backup/cmd/version.go +++ b/backup/cmd/version.go @@ -14,9 +14,9 @@ func buildVersionCommand() *cobra.Command { Short: "Prints the rsync version, protocol version, and full path to the rsync binary.", Run: func(cmd *cobra.Command, args []string) { rsyncPath, _ := cmd.Flags().GetString("rsync-path") - rsync := internal.NewRSyncCommand(rsyncPath) + rsync := internal.NewSyncCommand(rsyncPath, "") - output, err := rsync.GetVersionInfo() + output, _, err := rsync.GetVersionInfo() if err != nil { fmt.Printf("%v\n", err) diff --git a/backup/internal/config.go b/backup/internal/config.go index 6df79e7..dc3a347 100644 --- a/backup/internal/config.go +++ b/backup/internal/config.go @@ -26,6 +26,22 @@ func (cfg Config) String() string { return string(out) } +func (cfg Config) Apply(rsync JobCommand, logger *log.Logger) { + versionInfo, fullpath, err := rsync.GetVersionInfo() + if err != nil { + logger.Printf("Failed to fetch rsync version: %v", err) + } else { + logger.Printf("Rsync Binary Path: %s", fullpath) + logger.Printf("Rsync Version Info: %s", versionInfo) + } + + for _, job := range cfg.Jobs { + status := job.Apply(rsync) + logger.Printf("STATUS [%s]: %s", job.Name, status) + fmt.Printf("Status [%s]: %s\n", job.Name, status) + } +} + func LoadConfig(reader io.Reader) (Config, error) { var cfg Config diff --git a/backup/internal/helper.go b/backup/internal/helper.go index 0b9aed8..752e6f0 100644 --- a/backup/internal/helper.go +++ b/backup/internal/helper.go @@ -2,8 +2,6 @@ package internal import ( - "fmt" - "io" "log" "os" "strings" @@ -14,10 +12,10 @@ func NormalizePath(path string) string { return strings.TrimSuffix(strings.ReplaceAll(path, "//", "/"), "/") } -const FilePermission = 0644 +const LogFilePermission = 0644 const LogDirPermission = 0755 -func GetLogPath() string { +func getLogPath() string { logPath := "logs/sync-" + time.Now().Format("2006-01-02T15-04-05") err := os.MkdirAll(logPath, LogDirPermission) @@ -28,12 +26,12 @@ func GetLogPath() string { return logPath } -func createFileLogger() (*log.Logger, string) { - logPath := GetLogPath() +func CreateMainLogger() (*log.Logger, string) { + logPath := getLogPath() overallLogPath := logPath + "/summary.log" - overallLogFile, err := os.OpenFile(overallLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, FilePermission) + overallLogFile, err := os.OpenFile(overallLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, LogFilePermission) if err != nil { log.Fatalf("Failed to open overall log file: %v", err) } @@ -42,30 +40,3 @@ func createFileLogger() (*log.Logger, string) { return logger, logPath } - -func createLogger(rsync RSyncCommand) (*log.Logger, string) { - if rsync.ListOnly { - return log.New(io.Discard, "", 0), "" - } - - return createFileLogger() -} - -func (cfg Config) Apply(rsync RSyncCommand) { - overallLogger, logPath := createLogger(rsync) - - versionInfo, err := rsync.GetVersionInfo() - if err != nil { - overallLogger.Printf("Failed to fetch rsync version: %v", err) - } else { - overallLogger.Printf("Rsync Binary Path: %s", rsync.BinPath) - overallLogger.Printf("Rsync Version Info: %s", versionInfo) - } - - for _, job := range cfg.Jobs { - jobLogPath := fmt.Sprintf("%s/job-%s.log", logPath, job.Name) - status := job.Apply(rsync, jobLogPath) - overallLogger.Printf("STATUS [%s]: %s", job.Name, status) - fmt.Printf("Status [%s]: %s\n", job.Name, status) - } -} diff --git a/backup/internal/job.go b/backup/internal/job.go index 41fb004..e922c50 100644 --- a/backup/internal/job.go +++ b/backup/internal/job.go @@ -1,9 +1,56 @@ package internal -func (job Job) Apply(rsync RSyncCommand, logPath string) string { +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// JobYAML is a helper struct for proper YAML unmarshaling with defaults. +type JobYAML struct { + Name string `yaml:"name"` + Source string `yaml:"source"` + Target string `yaml:"target"` + Delete *bool `yaml:"delete"` + Enabled *bool `yaml:"enabled"` + Exclusions []string `yaml:"exclusions,omitempty"` +} + +func (job Job) Apply(rsync JobCommand) string { if !job.Enabled { return "SKIPPED" } - return rsync.Run(job, logPath) + return rsync.Run(job) +} + +// UnmarshalYAML implements custom YAML unmarshaling to handle defaults properly. +func (job *Job) UnmarshalYAML(node *yaml.Node) error { + var jobYAML JobYAML + + err := node.Decode(&jobYAML) + if err != nil { + return fmt.Errorf("failed to decode YAML node: %w", err) + } + + // Copy basic fields + job.Name = jobYAML.Name + job.Source = jobYAML.Source + job.Target = jobYAML.Target + job.Exclusions = jobYAML.Exclusions + + // Handle boolean fields with defaults + if jobYAML.Delete != nil { + job.Delete = *jobYAML.Delete + } else { + job.Delete = true // default value + } + + if jobYAML.Enabled != nil { + job.Enabled = *jobYAML.Enabled + } else { + job.Enabled = true // default value + } + + return nil } diff --git a/backup/internal/rsync.go b/backup/internal/rsync.go index 1b26db0..a1cd11e 100644 --- a/backup/internal/rsync.go +++ b/backup/internal/rsync.go @@ -10,58 +10,114 @@ import ( var ErrInvalidRsyncVersion = errors.New("invalid rsync version output") var ErrInvalidRsyncPath = errors.New("rsync path must be an absolute path") -type RSyncCommand struct { - BinPath string - Simulate bool - ListOnly bool +type SyncCommand struct { + BinPath string + BaseLogPath string + Executor JobRunner } -func NewRSyncCommand(binPath string) RSyncCommand { - return RSyncCommand{ - BinPath: binPath, - Executor: &RealSync{}, +func NewSyncCommand(binPath string, logPath string) SyncCommand { + return SyncCommand{ + BinPath: binPath, + BaseLogPath: logPath, + Executor: &RealSync{}, + } +} + +func (command SyncCommand) Run(job Job) string { + logPath := fmt.Sprintf("%s/job-%s.log", command.BaseLogPath, job.Name) + + args := ArgumentsForJob(job, logPath, false) + + return command.RunWithArgs(job, args) +} + +func (command SyncCommand) PrintArgs(job Job, args []string) { + fmt.Printf("Job: %s\n", job.Name) + fmt.Printf("Command: %s %s\n", command.BinPath, strings.Join(args, " ")) +} + +func (command SyncCommand) RunWithArgs(job Job, args []string) string { + command.PrintArgs(job, args) + + out, err := command.Executor.Execute(command.BinPath, args...) + fmt.Printf("Output:\n%s\n", string(out)) + + if err != nil { + return "FAILURE" } + + return "SUCCESS" +} + +type SimulateCommand struct { + SyncCommand } -func NewRSyncSimulateCommand(binPath string) RSyncCommand { - return RSyncCommand{ - BinPath: binPath, - Simulate: true, - Executor: &RealSync{}, +func NewSimulateCommand(binPath string, logPath string) SimulateCommand { + return SimulateCommand{ + SyncCommand: SyncCommand{ + BinPath: binPath, + BaseLogPath: logPath, + Executor: &RealSync{}, + }, } } -func NewListCommand(binPath string) RSyncCommand { - return RSyncCommand{ - BinPath: binPath, - ListOnly: true, - Executor: &RealSync{}, +func (command SimulateCommand) Run(job Job) string { + logPath := fmt.Sprintf("%s/job-%s.log", command.BaseLogPath, job.Name) + + args := ArgumentsForJob(job, logPath, true) + + return command.RunWithArgs(job, args) +} + +type ListCommand struct { + SyncCommand +} + +func NewListCommand(binPath string) ListCommand { + return ListCommand{ + SyncCommand: SyncCommand{ + BinPath: binPath, + BaseLogPath: "", + Executor: &RealSync{}, + }, } } +func (command ListCommand) Run(job Job) string { + logPath := fmt.Sprintf("%s/job-%s.log", command.BaseLogPath, job.Name) + + args := ArgumentsForJob(job, logPath, false) + command.PrintArgs(job, args) -func (command RSyncCommand) GetVersionInfo() (string, error) { + return "SUCCESS" +} + +func (command SyncCommand) GetVersionInfo() (string, string, error) { rsyncPath := command.BinPath if !filepath.IsAbs(rsyncPath) { - return "", fmt.Errorf("%w: \"%s\"", ErrInvalidRsyncPath, rsyncPath) + return "", "", fmt.Errorf("%w: \"%s\"", ErrInvalidRsyncPath, rsyncPath) } - output, err := command.Executor.Execute("--version") + output, err := command.Executor.Execute(command.BinPath, "--version") if err != nil { - return "", fmt.Errorf("error fetching rsync version: %w", err) + return "", "", fmt.Errorf("error fetching rsync version: %w", err) } // Validate output if !strings.Contains(string(output), "rsync") || !strings.Contains(string(output), "protocol version") { - return "", fmt.Errorf("%w: %s", ErrInvalidRsyncVersion, output) + return "", "", fmt.Errorf("%w: %s", ErrInvalidRsyncVersion, output) } - return string(output), nil + return string(output), rsyncPath, nil } func ArgumentsForJob(job Job, logPath string, simulate bool) []string { args := []string{"-aiv", "--stats"} + if job.Delete { args = append(args, "--delete") } @@ -81,22 +137,3 @@ func ArgumentsForJob(job Job, logPath string, simulate bool) []string { return args } - -func (rsync RSyncCommand) Run(job Job, logPath string) string { - args := ArgumentsForJob(job, logPath, rsync.Simulate) - fmt.Printf("Job: %s\n", job.Name) - fmt.Printf("Command: rsync %s %s\n", rsync.BinPath, strings.Join(args, " ")) - - if rsync.ListOnly { - return "SUCCESS" - } - - out, err := rsync.Executor.Execute(rsync.BinPath, args...) - fmt.Printf("Output:\n%s\n", string(out)) - - if err != nil { - return "FAILURE" - } - - return "SUCCESS" -} diff --git a/backup/internal/test/job_test.go b/backup/internal/test/job_test.go index 6f167b6..217d4f0 100644 --- a/backup/internal/test/job_test.go +++ b/backup/internal/test/job_test.go @@ -60,16 +60,24 @@ func WithExclusions(exclusions []string) Option { } } -func newMockRSyncCommand(simulate bool) internal.RSyncCommand { - return internal.RSyncCommand{ +func newMockSyncCommand() internal.SyncCommand { + return internal.SyncCommand{ BinPath: "/usr/bin/rsync", - Simulate: simulate, Executor: &MockCommandExecutor{}, } } +func newMockSimulateCommand() internal.SimulateCommand { + return internal.SimulateCommand{ + SyncCommand: internal.SyncCommand{ + BinPath: "/usr/bin/rsync", + Executor: &MockCommandExecutor{}, + }, + } +} + func TestApply(t *testing.T) { - rsync := newMockRSyncCommand(true) + rsync := newMockSimulateCommand() job := *NewJob( WithName("test_job"), @@ -78,14 +86,11 @@ func TestApply(t *testing.T) { WithExclusions([]string{"*.tmp"}), ) - status := job.Apply(rsync, "") + status := job.Apply(rsync) assert.Equal(t, statusSuccess, status) } func TestApply_Disabled(t *testing.T) { - command := internal.RSyncCommand{ - BinPath: "/usr/bin/rsync", - Executor: &MockCommandExecutor{}, - } + command := newMockSyncCommand() disabledJob := *NewJob( WithName("disabled_job"), @@ -94,12 +99,12 @@ func TestApply_Disabled(t *testing.T) { WithEnabled(false), ) - status := disabledJob.Apply(command, "") + status := disabledJob.Apply(command) assert.Equal(t, "SKIPPED", status) } func TestApply_Invalid(t *testing.T) { - rsync := newMockRSyncCommand(false) + rsync := newMockSyncCommand() // Test case for failure (simulate by providing invalid source path) invalidJob := *NewJob( @@ -108,12 +113,12 @@ func TestApply_Invalid(t *testing.T) { WithTarget("/mnt/backup1/invalid/"), ) - status := invalidJob.Apply(rsync, "") + status := invalidJob.Apply(rsync) assert.Equal(t, "FAILURE", status) } func TestJobSkippedEnabledTrue(t *testing.T) { - rsync := newMockRSyncCommand(false) + rsync := newMockSyncCommand() job := *NewJob( WithName("test_job"), @@ -121,12 +126,12 @@ func TestJobSkippedEnabledTrue(t *testing.T) { WithTarget("/mnt/backup1/test/"), ) - status := job.Apply(rsync, "") + status := job.Apply(rsync) assert.Equal(t, statusSuccess, status) } func TestJobSkippedEnabledFalse(t *testing.T) { - rsync := newMockRSyncCommand(false) + rsync := newMockSyncCommand() disabledJob := *NewJob( WithName("disabled_job"), @@ -135,12 +140,12 @@ func TestJobSkippedEnabledFalse(t *testing.T) { WithEnabled(false), ) - status := disabledJob.Apply(rsync, "") + status := disabledJob.Apply(rsync) assert.Equal(t, "SKIPPED", status) } func TestJobSkippedEnabledOmitted(t *testing.T) { - rsync := newMockRSyncCommand(false) + rsync := newMockSyncCommand() job := *NewJob( WithName("omitted_enabled_job"), @@ -148,13 +153,13 @@ func TestJobSkippedEnabledOmitted(t *testing.T) { WithTarget("/mnt/backup1/omitted/"), ) - status := job.Apply(rsync, "") + status := job.Apply(rsync) assert.Equal(t, statusSuccess, status) } func TestApplyWithMockedRsync(t *testing.T) { mockExecutor := &MockCommandExecutor{} - rsync := newMockRSyncCommand(true) + rsync := newMockSimulateCommand() rsync.Executor = mockExecutor job := *NewJob( @@ -163,7 +168,7 @@ func TestApplyWithMockedRsync(t *testing.T) { WithTarget("/mnt/backup1/test/"), WithExclusions([]string{"*.tmp"}), ) - status := job.Apply(rsync, "") + status := job.Apply(rsync) assert.Equal(t, statusSuccess, status) assert.NotEmpty(t, mockExecutor.CapturedCommands) diff --git a/backup/internal/test/rsync_test.go b/backup/internal/test/rsync_test.go index b049dfa..fcfbf9b 100644 --- a/backup/internal/test/rsync_test.go +++ b/backup/internal/test/rsync_test.go @@ -32,7 +32,7 @@ func TestArgumentsForJob(t *testing.T) { } func TestGetVersionInfo_Success(t *testing.T) { - rsync := internal.RSyncCommand{ + rsync := internal.SyncCommand{ BinPath: rsyncPath, Executor: &MockCommandExecutor{ Output: "rsync version 3.2.3 protocol version 31\n", @@ -40,14 +40,15 @@ func TestGetVersionInfo_Success(t *testing.T) { }, } - versionInfo, err := rsync.GetVersionInfo() + versionInfo, fullpath, err := rsync.GetVersionInfo() require.NoError(t, err) + assert.Equal(t, rsyncPath, fullpath) assert.Equal(t, "rsync version 3.2.3 protocol version 31\n", versionInfo) } func TestGetVersionInfo_CommandError(t *testing.T) { - rsync := internal.RSyncCommand{ + rsync := internal.SyncCommand{ BinPath: rsyncPath, Executor: &MockCommandExecutor{ Output: "", @@ -55,14 +56,15 @@ func TestGetVersionInfo_CommandError(t *testing.T) { }, } - versionInfo, err := rsync.GetVersionInfo() + versionInfo, fullpath, err := rsync.GetVersionInfo() require.Error(t, err) + assert.Empty(t, fullpath) assert.Empty(t, versionInfo) } func TestGetVersionInfo_InvalidOutput(t *testing.T) { - rsync := internal.RSyncCommand{ + rsync := internal.SyncCommand{ BinPath: rsyncPath, Executor: &MockCommandExecutor{ Output: "invalid output", @@ -70,14 +72,15 @@ func TestGetVersionInfo_InvalidOutput(t *testing.T) { }, } - versionInfo, err := rsync.GetVersionInfo() + versionInfo, fullpath, err := rsync.GetVersionInfo() require.Error(t, err) + assert.Empty(t, fullpath) assert.Empty(t, versionInfo) } func TestGetVersionInfo_EmptyPath(t *testing.T) { - rsync := internal.RSyncCommand{ + rsync := internal.SyncCommand{ BinPath: "", Executor: &MockCommandExecutor{ Output: "", @@ -85,15 +88,16 @@ func TestGetVersionInfo_EmptyPath(t *testing.T) { }, } - versionInfo, err := rsync.GetVersionInfo() + versionInfo, fullpath, err := rsync.GetVersionInfo() require.Error(t, err) require.EqualError(t, err, "rsync path must be an absolute path: \"\"") assert.Empty(t, versionInfo) + assert.Empty(t, fullpath) } func TestGetVersionInfo_IncompletePath(t *testing.T) { - rsync := internal.RSyncCommand{ + rsync := internal.SyncCommand{ BinPath: "bin/rsync", Executor: &MockCommandExecutor{ Output: "", @@ -101,9 +105,10 @@ func TestGetVersionInfo_IncompletePath(t *testing.T) { }, } - versionInfo, err := rsync.GetVersionInfo() + versionInfo, fullpath, err := rsync.GetVersionInfo() require.Error(t, err) require.EqualError(t, err, "rsync path must be an absolute path: \"bin/rsync\"") assert.Empty(t, versionInfo) + assert.Empty(t, fullpath) } diff --git a/backup/internal/types.go b/backup/internal/types.go index 1326999..5d0e840 100644 --- a/backup/internal/types.go +++ b/backup/internal/types.go @@ -1,13 +1,5 @@ package internal -import ( - "fmt" - - "gopkg.in/yaml.v3" -) - -// Centralized type definitions - // Path represents a source or target path with optional exclusions. type Path struct { Path string `yaml:"path"` @@ -34,43 +26,7 @@ type Job struct { Exclusions []string `yaml:"exclusions,omitempty"` } -// JobYAML is a helper struct for proper YAML unmarshaling with defaults. -type JobYAML struct { - Name string `yaml:"name"` - Source string `yaml:"source"` - Target string `yaml:"target"` - Delete *bool `yaml:"delete"` - Enabled *bool `yaml:"enabled"` - Exclusions []string `yaml:"exclusions,omitempty"` -} - -// UnmarshalYAML implements custom YAML unmarshaling to handle defaults properly. -func (j *Job) UnmarshalYAML(node *yaml.Node) error { - var jobYAML JobYAML - - err := node.Decode(&jobYAML) - if err != nil { - return fmt.Errorf("failed to decode YAML node: %w", err) - } - - // Copy basic fields - j.Name = jobYAML.Name - j.Source = jobYAML.Source - j.Target = jobYAML.Target - j.Exclusions = jobYAML.Exclusions - - // Handle boolean fields with defaults - if jobYAML.Delete != nil { - j.Delete = *jobYAML.Delete - } else { - j.Delete = true // default value - } - - if jobYAML.Enabled != nil { - j.Enabled = *jobYAML.Enabled - } else { - j.Enabled = true // default value - } - - return nil +type JobCommand interface { + Run(job Job) string + GetVersionInfo() (string, string, error) }