Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backup/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cmd

import (
"backup-rsync/backup/internal"
"io"
"log"

"github.com/spf13/cobra"
)
Expand All @@ -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)
},
}
}
5 changes: 3 additions & 2 deletions backup/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}
}
5 changes: 3 additions & 2 deletions backup/cmd/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}
}
4 changes: 2 additions & 2 deletions backup/cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 16 additions & 0 deletions backup/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
39 changes: 5 additions & 34 deletions backup/internal/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
package internal

import (
"fmt"
"io"
"log"
"os"
"strings"
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -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)
}
}
51 changes: 49 additions & 2 deletions backup/internal/job.go
Original file line number Diff line number Diff line change
@@ -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
}
123 changes: 80 additions & 43 deletions backup/internal/rsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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"
}
Loading