package syncer import ( "bytes" "fmt" "log" "os/exec" "path/filepath" "mediawatcher/internal/classifier" "mediawatcher/internal/config" ) type Syncer struct { cfg *config.SyncConfig } func New(cfg *config.SyncConfig) *Syncer { if cfg == nil || !cfg.Enabled { return nil } return &Syncer{cfg: cfg} } func (s *Syncer) Sync(res classifier.Result) { if s == nil || !s.cfg.Enabled { return } for _, t := range s.cfg.Targets { if err := s.syncToTarget(t, res); err != nil { log.Printf("[sync] error syncing to %s: %v", t.Host, err) } } } func (s *Syncer) syncToTarget(t config.RsyncTarget, res classifier.Result) error { if t.Host == "" || t.DestBase == "" { return fmt.Errorf("invalid target config: host and dest_base required") } user := t.User if user == "" { user = "media" } port := t.Port if port == 0 { port = 22 } // Recreate relative dest under DestBase rel, err := filepath.Rel(res.DestDir, filepath.Join(res.DestDir, res.DestName)) if err != nil { rel = res.DestName } remoteDir := t.DestBase remote := fmt.Sprintf("%s@%s:%s/", user, t.Host, remoteDir) args := []string{"-avh"} if port != 22 { args = append(args, "-e", fmt.Sprintf("ssh -p %d", port)) } args = append(args, t.ExtraArgs...) args = append(args, filepath.Join(res.DestDir, res.DestName), remote) log.Printf("[sync] rsync %v", args) cmd := exec.Command(s.cfg.RsyncBinary, args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { log.Printf("[sync] stdout: %s", stdout.String()) log.Printf("[sync] stderr: %s", stderr.String()) return fmt.Errorf("rsync failed: %w", err) } return nil }